diff --git a/doc/asyncenumerable.md b/doc/asyncenumerable.md index e4fb244a..43950aa2 100644 --- a/doc/asyncenumerable.md +++ b/doc/asyncenumerable.md @@ -54,6 +54,26 @@ A remoted `IAsyncEnumerable` can only be enumerated once. Calling `IAsyncEnumerable.GetAsyncEnumerator(CancellationToken)` more than once will result in an `InvalidOperationException` being thrown. +When *not* using the dynamically generated proxies, acquiring and enumerating an `IAsyncEnumerator` looks like this: + +```cs +var enumerable = await this.clientRpc.InvokeWithCancellationAsync>( + "GetNumbersAsync", cancellationToken); + +await foreach (var item in enumerable.WithCancellation(cancellationToken)) +{ + // processing +} +``` + +We pass `cancellationToken` into `InvokeWithCancellationAsync` so we can cancel the initial call. +We pass it again to the `WithCancellation` extension method inside the `foreach` expression +so that the token is applied to each iteration of the loop over the enumerable when +we may be awaiting a network call. + +Using the `WithCancellation` extension method is not necessary when using dynamically generated proxies +because they automatically propagate the token from the first call to the enumerator. + ### Transmitting large collections Most C# iterator methods return `IEnumerable` and produce values synchronously. @@ -114,6 +134,11 @@ To improve performance across a network, this behavior can be modified: the consumer's request for them. This allows for the possibility that the server is processing the data while the last value(s) are in transit to the client or being processed by the client. This improves performance when the time to generate the values is significant. +1. **Prefetch**: The generator collects some number of values up front and _includes_ them + in the initial message with the token for acquiring more values. + While "read ahead" reduces the time the consumer must wait while the generator produces the values + for each request, this prefetch setting entirely eliminates the latency of a round-trip for just + the first set of items. The above optimizations are configured individually and may be used in combination. @@ -168,6 +193,22 @@ In short: `MinBatchSize` guarantees a minimum number of values the server will s the sequence is finished, and `MaxReadAhead` is how many values may be produced on the server in anticipation of the next request from the client for more values. +As the prefetch feature requires an asynchronous operation itself to fill a cache of items for transmission +to the receiver, it is not merely a setting but a separate extension method: `WithPrefetchAsync`. +It can be composed with `WithJsonRpcSettings` in either order, but it's syntactically simplest to add last +since it is an async method: + +```cs +public async ValueTask> GenerateNumbersAsync(CancellationToken cancellationToken) +{ + return await this.GenerateNumbersCoreAsync(cancellationToken) + .WithJsonRpcSettings(new JsonRpcEnumerableSettings { MinBatchSize = 10 }) + .WithPrefetchAsync(count: 20, cancellationToken); +} +``` + +Please refer to the section on RPC interfaces below for a discussion on the return type of the above method. + The state machine and any cached values are released from the generator when the `IAsyncEnumerator` is disposed. Customized settings must be applied at the *generator* side. They are ignored if applied to the consumer side. @@ -182,6 +223,106 @@ public IAsyncEnumerable GetNumbersAsync(int batchSize) The above delegates to an C# iterator method, but decorates the result with a batch size determined by the client. +## RPC interfaces + +When an async iterator method can be written to return `IAsyncEnumerator` directly, +it makes for a natural implementation of the ideal C# interface, such as: + +```cs +interface IService +{ + IAsyncEnumerable GetNumbersAsync(CancellationToken cancellationToken); +} +``` + +This often can be implemented as simply as: + +```cs +public async IAsyncEnumerable GetNumbersAsync([EnumeratorCancellation] CancellationToken cancellationToken) +{ + for (int i = 1; i <= 20; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + await Task.Yield(); + yield return i; + } +} +``` + +But when applying the perf modifiers, additional steps must be taken: + +1. Rename the C# iterator method and (optionally) make it private. +1. Expose a new implementation of the interface method which calls the inner one and applies the modifications. + +```cs +public IAsyncEnumerable GetNumbersAsync(CancellationToken cancellationToken) +{ + return this.GetNumbersCoreAsync(cancellationToken) + .WithJsonRpcSettings(new JsonRpcEnumerableSettings { MinBatchSize = batchSize }); +} + +private async IAsyncEnumerable GetNumbersCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) +{ + for (int i = 1; i <= 20; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + await Task.Yield(); + yield return i; + } +} +``` + +The above isn't too inconvenient, but it is a bit of extra work. It still can implement the same interface that is shared with the client. +But it gets more complicated when adding `WithPrefetchAsync`, since now the wrapper method itself contains an `await`. +It cannot simply return `IAsyncEnumerable` since it is not using `yield return` but rather it's returning a specially-decorated +`IAsyncEnumerable` instance. +So instead, we have to use `async ValueTask>` as the return type: + +```cs +public async ValueTask> GetNumbersAsync(CancellationToken cancellationToken) +{ + return await this.GetNumbersCoreAsync(cancellationToken) + .WithPrefetchAsync(10); +} + +private async IAsyncEnumerable GetNumbersCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) +{ + for (int i = 1; i <= 20; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + await Task.Yield(); + yield return i; + } +} +``` + +But now we've changed the method signature of the primary method that JSON-RPC will invoke and we can't implement the C# interface +as it was originally. We can change the interface to also return `ValueTask>` but then that changes +the client's calling pattern to add an extra *await* (the one after `in`): + +```cs +await foreach(var item in await serverProxy.GetNumbersAsync(cancellationToken)) +{ + // +} +``` + +That's a little more awkward to write. If we want to preserve the original consuming syntax, we can: + +1. Keep the interface as simply returning `IAsyncEnumerable`. +1. Stop implementing the interface on the server class itself. +1. Change the server method to return `async ValueTask>` as required for `WithPrefetchAsync`. + +This is a trade-off between convenience in maintaining and verifying the JSON-RPC server actually satisfies +the required interface vs. the client writing the most natural C# `await foreach` code. + +Note that while the proxy generator requires that _all_ methods return either `Task{}`, `ValueTask{}` or +`IAsyncEnumerable`, the server class itself can return any type, and StreamJsonRpc will ultimately +make it appear to the client to be asynchronous. For example a server method can return `int` while the client +proxy interface is written as if the server method returned `Task` and it will work just fine. +This is why the interface method can return `IAsyncEnumerable` while the server class method can actually +return `ValueTask>` without there being a problem. + ## Resource leaks concerns The most important consideration is that of resource leaks. The party that transmits the `IAsyncEnumerable` @@ -246,23 +387,35 @@ Always make sure the client is expecting the `IAsyncEnumerable` when the serv This section is primarily for JSON-RPC library authors that want to interop with StreamJsonRpc's async enumerable feature. +An `IAsyncEnumerable` object may be included within or as an RPC method argument or return value. +We use the terms `generator` to refer to the sender and `consumer` to refer to the receiver. + +Capitalized words are key words per [RFC 2119](https://tools.ietf.org/html/rfc2119). + ### Originating message -A JSON-RPC message that carries an `IAsyncEnumerator` provides a token to the enumerator -so that the message receiver can use this token to request values from or dispose the remote enumerator. -This token may be any valid JSON token except `null`: +A JSON-RPC message that carries an `IAsyncEnumerator` encodes it as a JSON object. +The JSON object may contain these properties: -A result that returns an `IAsyncEnumerable` would look something like this: +| property | description | +|--|--| +| `token` | Any valid JSON token except `null` to be used to request additional values or dispose of the enumerator. This property is **required** if there are more values than those included in this message, and must be absent or `null` if all values are included in the message. +| `values` | A JSON array of the first batch of values. This property is **optional** or may be specified as `null` or an empty array. A lack of values here does not signify the enumerable is empty but rather that the consumer must explicitly request them. + +A result that returns an `IAsyncEnumerable` would look something like this if it included the first few items and more might be available should the receiver ask for them: ```json { "jsonrpc": "2.0", "id": 1, - "result": "enum-handle" + "result": { + "token": "enum-handle", + "values": [ 1, 2, 3 ] + } } ``` -A request that includes an `IAsyncEnumerable` as a method argument might look like this: +A request that includes an `IAsyncEnumerable` as a method argument might look like this if it included the first few items and more might be available should the receiver ask for them: ```json { @@ -271,7 +424,10 @@ A request that includes an `IAsyncEnumerable` as a method argument might look "method": "FooAsync", "params": [ "hi", - "enum-handle" + { + "token": "enum-handle", + "values": [ 1, 2, 3 ] + } ] } ``` @@ -284,19 +440,50 @@ or method argument: "jsonrpc": "2.0", "id": 1, "result": { - "enumerable": "enum-handle", + "enumerable": { + "token": "enum-handle", + "values": [ 1, 2, 3 ] + }, "count": 10 } } ``` -A client should not send an `IAsyncEnumerable` object in a notification, since that would lead to +The enumerable certainly may include no pre-fetched values. This object (which may appear in any of the above contexts) demonstrates this: + +```json +{ + "token": "enum-handle" +} +``` + +The inclusion of the `token` property signifies that the receiver should query for more values or dispose of the enumerable. + +Alternatively if the prefetched values are known to include *all* values such that the receiver need not ask for more, +we would have just the other property: + +```json +{ + "values": [ 1, 2, 3 ] +} +``` + +Finally, if the enumerable is known to be empty, the object may be completely empty: + +```json +{ +} +``` + +A client SHOULD NOT send an `IAsyncEnumerable` object in a notification, since that would lead to a memory leak on the client if the server does not handle a particular method or throws before it could process the enumerable. +The generator MAY pass multiple `IAsyncEnumerable` instances in a single JSON-RPC message. + ### Consumer request for values -A request from the consumer to the producer for (more) value(s) is done via a standard JSON-RPC +A request from the consumer to the generator for (more) value(s) is done via a standard JSON-RPC request method call with `$/enumerator/next` as the method name and one argument that carries the enumerator token. When using named arguments this is named `token`. @@ -320,46 +507,71 @@ or: } ``` -The generator should respond to this request with an error containing `error.code = -32001` -when the specified enumeration token does not exist, possibly because it has already been disposed. +The consumer MUST NOT send this message after receiving a message related to this enumerable +with `finished: true` in it. +The consumer MUST NOT send this message for a given enumerable +while waiting for a response to a previous request for the same enumerable, +since the generator may respond to an earlier request with `finished: true`. + +The consumer MAY cancel a request using the `$/cancelRequest` method as described elsewhere. +The consumer MUST continue the enumeration or dispose it if the server responds with a +result rather than a cancellation error. -### Producer's response with values +The generator SHOULD respond to this request with an error containing `error.code = -32001` +when the specified enumeration token does not exist, possibly because it has already been disposed +or because the last set of values provided to the consumer included `finished: true`. -A response with value(s) from the producer always comes as an object that contains an array of values -(even if only one element is provided) and a boolean indicating whether the last value has been provided: +### Generator's response with values + +A response with value(s) from the generator is encoded as a JSON object. +The JSON object may contain these properties: + +| property | description | +|--|--| +| values | A JSON array of values. This value is **required.** +| finished | A boolean value indicating whether the last value from the enumerable has been returned. This value is **optional** and defaults to `false`. + +Here is an example of a result encoded as a JSON object: ```json { "jsonrpc": "2.0", "id": 2, "result": { - "values": [ 1, 2, 3 ], + "values": [ 4, 5, 6 ], "finished": false } } ``` -Note the `finished` property is a hint from the producer for when the last value has been returned. -The server *may* not know that the last value has been returned and should specify `false` unless it is sure -the last value has been produced. +The server MUST specify `finished: true` only when it is sure the last value in the enumerable has been returned. +The server SHOULD release all resources related to the enumerable and token when doing so. -The client *may* ask for more values without regard to the `finished` property, but if `finished: true` -the client may optimize to not ask for more values but instead go directly to dispose of the enumerator. +The server MAY specify `finished: false` in one response and `values: [], finished: true` in the next response. -The server should never return an empty array of values unless the last value in the sequence has already +The consumer MUST NOT ask for more values when `finished` is `true` or an error response is received. +The generator MAY respond with an error if this is done. + +The generator should never return an empty array of values unless the last value in the sequence has already been returned to the client. ### Consumer disposes enumerator -The consumer always notifies the producer when the local enumerator proxy is disposed -by invoking the `$/enumerator/dispose` method. +When the consumer aborts enumeration before the generator has sent `finished: true`, +the consumer MUST send a disposal message to release resources held by the generator +unless the generator has already responded with an error message to a previous request for values. + +The consumer does this by invoking the `$/enumerator/abort` JSON-RPC method on the generator. The arguments follow the same schema as the `$/enumerator/next` method. -This *may* be a notification. +This MAY be a notification. ```json { "jsonrpc": "2.0", - "method": "$/enumerator/dispose", + "method": "$/enumerator/abort", "params": { "token": "enum-handle" }, } ``` + +The generator SHOULD release resources upon receipt of the disposal message. +The generator SHOULD reject any disposal request received after sending a `finished: true` message. diff --git a/src/StreamJsonRpc.Tests/AsyncEnumerableTests.cs b/src/StreamJsonRpc.Tests/AsyncEnumerableTests.cs index 738b9d7f..7492c964 100644 --- a/src/StreamJsonRpc.Tests/AsyncEnumerableTests.cs +++ b/src/StreamJsonRpc.Tests/AsyncEnumerableTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.Globalization; using System.Runtime.CompilerServices; @@ -37,26 +38,34 @@ protected AsyncEnumerableTests(ITestOutputHelper logger) /// /// This interface should NOT be implemented by , - /// since the server implements the one method on this interface with a return type of Task{T} + /// since the server implements the methods on this interface with a return type of Task{T} /// but we want the client proxy to NOT be that. /// protected interface IServer2 { IAsyncEnumerable WaitTillCanceledBeforeReturningAsync(CancellationToken cancellationToken); + + IAsyncEnumerable GetNumbersParameterizedAsync(int batchSize, int readAhead, int prefetch, int totalCount, CancellationToken cancellationToken); } protected interface IServer { + IAsyncEnumerable GetValuesFromEnumeratedSourceAsync(CancellationToken cancellationToken); + IAsyncEnumerable GetNumbersInBatchesAsync(CancellationToken cancellationToken); IAsyncEnumerable GetNumbersWithReadAheadAsync(CancellationToken cancellationToken); IAsyncEnumerable GetNumbersAsync(CancellationToken cancellationToken); + IAsyncEnumerable GetNumbersNoCancellationAsync(); + IAsyncEnumerable WaitTillCanceledBeforeFirstItemAsync(CancellationToken cancellationToken); Task> WaitTillCanceledBeforeReturningAsync(CancellationToken cancellationToken); + Task> WaitTillCanceledBeforeFirstItemWithPrefetchAsync(CancellationToken cancellationToken); + Task GetNumbersAndMetadataAsync(CancellationToken cancellationToken); Task PassInNumbersAsync(IAsyncEnumerable numbers, CancellationToken cancellationToken); @@ -78,6 +87,7 @@ public Task InitializeAsync() this.serverRpc = new JsonRpc(serverHandler, this.server); this.clientRpc = new JsonRpc(clientHandler); + // Don't use Verbose as it has nasty side-effects leading to test failures, which we need to fix! this.serverRpc.TraceSource = new TraceSource("Server", SourceLevels.Information); this.clientRpc.TraceSource = new TraceSource("Client", SourceLevels.Information); @@ -122,6 +132,20 @@ public async Task GetIAsyncEnumerableAsReturnType(bool useProxy) Assert.Equal(Server.ValuesReturnedByEnumerables, realizedValuesCount); } + [Fact] + public async Task GetIAsyncEnumerableAsReturnType_WithProxy_NoCancellation() + { + int realizedValuesCount = 0; + IAsyncEnumerable enumerable = this.clientProxy.Value.GetNumbersNoCancellationAsync(); + await foreach (int number in enumerable) + { + realizedValuesCount++; + this.Logger.WriteLine(number.ToString(CultureInfo.InvariantCulture)); + } + + Assert.Equal(Server.ValuesReturnedByEnumerables, realizedValuesCount); + } + [Theory] [PairwiseData] public async Task GetIAsyncEnumerableAsMemberWithinReturnType(bool useProxy) @@ -284,6 +308,44 @@ public async Task Cancellation_AfterFirstMoveNext(bool useProxy) await Assert.ThrowsAnyAsync(async () => await enumerator.MoveNextAsync()); } + [Fact] + public async Task Cancellation_AfterFirstMoveNext_NaturalForEach_Proxy() + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource(this.TimeoutToken); + IAsyncEnumerable enumerable = this.clientProxy.Value.GetNumbersAsync(cts.Token); + + int iterations = 0; + await Assert.ThrowsAsync(async delegate + { + await foreach (var item in enumerable) + { + iterations++; + cts.Cancel(); + } + }).WithCancellation(this.TimeoutToken); + + Assert.Equal(1, iterations); + } + + [Fact] + public async Task Cancellation_AfterFirstMoveNext_NaturalForEach_NoProxy() + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource(this.TimeoutToken); + var enumerable = await this.clientRpc.InvokeWithCancellationAsync>(nameof(Server.GetNumbersAsync), cancellationToken: cts.Token); + + int iterations = 0; + await Assert.ThrowsAsync(async delegate + { + await foreach (var item in enumerable.WithCancellation(cts.Token)) + { + iterations++; + cts.Cancel(); + } + }).WithCancellation(this.TimeoutToken); + + Assert.Equal(1, iterations); + } + [Theory] [PairwiseData] public async Task Cancellation_DuringLongRunningServerMoveNext(bool useProxy) @@ -297,17 +359,17 @@ public async Task Cancellation_DuringLongRunningServerMoveNext(bool useProxy) var moveNextTask = enumerator.MoveNextAsync(); Assert.False(moveNextTask.IsCompleted); cts.Cancel(); - await Assert.ThrowsAnyAsync(async () => await moveNextTask); + await Assert.ThrowsAnyAsync(async () => await moveNextTask).WithCancellation(this.TimeoutToken); } [Theory] [PairwiseData] - public async Task Cancellation_DuringLongRunningServerBeforeReturning(bool useProxy) + public async Task Cancellation_DuringLongRunningServerBeforeReturning(bool useProxy, bool prefetch) { var cts = new CancellationTokenSource(); Task> enumerable = useProxy - ? this.clientProxy.Value.WaitTillCanceledBeforeReturningAsync(cts.Token) - : this.clientRpc.InvokeWithCancellationAsync>(nameof(Server.WaitTillCanceledBeforeReturningAsync), cancellationToken: cts.Token); + ? (prefetch ? this.clientProxy.Value.WaitTillCanceledBeforeFirstItemWithPrefetchAsync(cts.Token) : this.clientProxy.Value.WaitTillCanceledBeforeReturningAsync(cts.Token)) + : this.clientRpc.InvokeWithCancellationAsync>(prefetch ? nameof(Server.WaitTillCanceledBeforeFirstItemWithPrefetchAsync) : nameof(Server.WaitTillCanceledBeforeReturningAsync), cancellationToken: cts.Token); // Make sure the method has been invoked first. await this.server.MethodEntered.WaitAsync(this.TimeoutToken); @@ -397,6 +459,35 @@ public async Task ArgumentEnumerable_ForciblyDisposedAndReleasedWhenNotDisposedW await Assert.ThrowsAsync(() => this.server.ArgEnumeratorAfterReturn).WithCancellation(this.TimeoutToken); } + [SkippableFact] + [Trait("GC", "")] + public async Task ReturnEnumerable_AutomaticallyReleasedOnErrorFromIteratorMethod() + { + WeakReference enumerable = await this.ReturnEnumerable_AutomaticallyReleasedOnErrorFromIteratorMethod_Helper(); + AssertCollectedObject(enumerable); + } + + [Theory] + [InlineData(1, 0, 2, Server.ValuesReturnedByEnumerables)] + [InlineData(2, 2, 2, Server.ValuesReturnedByEnumerables)] + [InlineData(2, 4, 2, Server.ValuesReturnedByEnumerables)] + [InlineData(2, 2, 4, Server.ValuesReturnedByEnumerables)] + [InlineData(2, 2, Server.ValuesReturnedByEnumerables, Server.ValuesReturnedByEnumerables)] + [InlineData(2, 2, Server.ValuesReturnedByEnumerables + 1, Server.ValuesReturnedByEnumerables)] + [InlineData(2, 2, 2, 0)] + [InlineData(2, 2, 2, 1)] + public async Task Prefetch(int minBatchSize, int maxReadAhead, int prefetch, int totalCount) + { + var proxy = this.clientRpc.Attach(); + int enumerated = 0; + await foreach (var item in proxy.GetNumbersParameterizedAsync(minBatchSize, maxReadAhead, prefetch, totalCount, this.TimeoutToken).WithCancellation(this.TimeoutToken)) + { + Assert.Equal(++enumerated, item); + } + + Assert.Equal(totalCount, enumerated); + } + protected abstract void InitializeFormattersAndHandlers(); private static void AssertCollectedObject(WeakReference weakReference) @@ -452,6 +543,28 @@ private async Task ArgumentEnumerable_ForciblyDisposedAndReleased return result; } + [MethodImpl(MethodImplOptions.NoInlining)] + private async Task ReturnEnumerable_AutomaticallyReleasedOnErrorFromIteratorMethod_Helper() + { + this.server.EnumeratedSource = ImmutableList.Create(1, 2, 3); + WeakReference weakReferenceToSource = new WeakReference(this.server.EnumeratedSource); + var cts = CancellationTokenSource.CreateLinkedTokenSource(this.TimeoutToken); + + // Start up th emethod and get the first item. + var enumerable = this.clientProxy.Value.GetValuesFromEnumeratedSourceAsync(cts.Token); + var enumerator = enumerable.GetAsyncEnumerator(cts.Token); + Assert.True(await enumerator.MoveNextAsync()); + + // Now remove the only strong reference to the source object other than what would be captured by the async iterator method. + this.server.EnumeratedSource = this.server.EnumeratedSource.Clear(); + + // Now array for the server method to be canceled + cts.Cancel(); + await Assert.ThrowsAsync(async () => await enumerator.MoveNextAsync()); + + return weakReferenceToSource; + } + protected class Server : IServer { /// @@ -476,23 +589,33 @@ protected class Server : IServer public AsyncManualResetEvent ValueGenerated { get; } = new AsyncManualResetEvent(); + public ImmutableList EnumeratedSource { get; set; } = ImmutableList.Empty; + + public async IAsyncEnumerable GetValuesFromEnumeratedSourceAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + foreach (var item in this.EnumeratedSource) + { + cancellationToken.ThrowIfCancellationRequested(); + await Task.Yield(); + yield return item; + } + } + public IAsyncEnumerable GetNumbersInBatchesAsync(CancellationToken cancellationToken) => this.GetNumbersAsync(cancellationToken).WithJsonRpcSettings(new JsonRpcEnumerableSettings { MinBatchSize = MinBatchSize }); public IAsyncEnumerable GetNumbersWithReadAheadAsync(CancellationToken cancellationToken) => this.GetNumbersAsync(cancellationToken).WithJsonRpcSettings(new JsonRpcEnumerableSettings { MaxReadAhead = MaxReadAhead, MinBatchSize = MinBatchSize }); + public IAsyncEnumerable GetNumbersNoCancellationAsync() => this.GetNumbersAsync(CancellationToken.None); + public async IAsyncEnumerable GetNumbersAsync([EnumeratorCancellation] CancellationToken cancellationToken) { try { - for (int i = 1; i <= ValuesReturnedByEnumerables; i++) + await foreach (var item in this.GetNumbersAsync(ValuesReturnedByEnumerables, cancellationToken)) { - cancellationToken.ThrowIfCancellationRequested(); - await Task.Yield(); - this.ActuallyGeneratedValueCount++; - this.ValueGenerated.PulseAll(); - yield return i; + yield return item; } } finally @@ -501,6 +624,13 @@ public async IAsyncEnumerable GetNumbersAsync([EnumeratorCancellation] Canc } } + public async ValueTask> GetNumbersParameterizedAsync(int batchSize, int readAhead, int prefetch, int totalCount, CancellationToken cancellationToken) + { + return await this.GetNumbersAsync(totalCount, cancellationToken) + .WithJsonRpcSettings(new JsonRpcEnumerableSettings { MinBatchSize = batchSize, MaxReadAhead = readAhead }) + .WithPrefetchAsync(prefetch, cancellationToken); + } + public async IAsyncEnumerable WaitTillCanceledBeforeFirstItemAsync([EnumeratorCancellation] CancellationToken cancellationToken) { var tcs = new TaskCompletionSource(); @@ -508,6 +638,13 @@ public async IAsyncEnumerable WaitTillCanceledBeforeFirstItemAsync([Enumera yield return 0; // we will never reach this. } + public async Task> WaitTillCanceledBeforeFirstItemWithPrefetchAsync(CancellationToken cancellationToken) + { + this.MethodEntered.Set(); + return await this.WaitTillCanceledBeforeFirstItemAsync(cancellationToken) + .WithPrefetchAsync(1, cancellationToken); + } + public Task> WaitTillCanceledBeforeReturningAsync(CancellationToken cancellationToken) { this.MethodEntered.Set(); @@ -550,6 +687,18 @@ public Task GetNumbersAndMetadataAsync(CancellationTok Enumeration = this.GetNumbersAsync(cancellationToken), }); } + + private async IAsyncEnumerable GetNumbersAsync(int totalCount, [EnumeratorCancellation] CancellationToken cancellationToken) + { + for (int i = 1; i <= totalCount; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + await Task.Yield(); + this.ActuallyGeneratedValueCount++; + this.ValueGenerated.PulseAll(); + yield return i; + } + } } [DataContract] diff --git a/src/StreamJsonRpc.Tests/JsonRpcExtensionsTests.cs b/src/StreamJsonRpc.Tests/JsonRpcExtensionsTests.cs index 4cc5457f..a5db724e 100644 --- a/src/StreamJsonRpc.Tests/JsonRpcExtensionsTests.cs +++ b/src/StreamJsonRpc.Tests/JsonRpcExtensionsTests.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; using StreamJsonRpc; using Xunit; @@ -12,6 +13,8 @@ public class JsonRpcExtensionsTests : TestBase { + private static readonly IReadOnlyList SmallList = new int[] { 1, 2, 3 }; + public JsonRpcExtensionsTests(ITestOutputHelper logger) : base(logger) { @@ -20,22 +23,121 @@ public JsonRpcExtensionsTests(ITestOutputHelper logger) [Fact] public async Task AsAsyncEnumerable() { - var collection = new int[] { 1, 2, 3 }; - int count = 0; - await foreach (int item in collection.AsAsyncEnumerable()) - { - Assert.Equal(++count, item); - } - - Assert.Equal(3, count); + await AssertExpectedValues(SmallList.AsAsyncEnumerable()); } [Fact] public async Task AsAsyncEnumerable_Settings() { - var collection = new int[] { 1, 2, 3 }; + await AssertExpectedValues(SmallList.AsAsyncEnumerable(new JsonRpcEnumerableSettings { MinBatchSize = 3 })); + } + + [Fact] + public async Task AsAsyncEnumerable_Cancellation() + { + var asyncEnum = SmallList.AsAsyncEnumerable(); + var cts = new CancellationTokenSource(); + + var enumerator = asyncEnum.GetAsyncEnumerator(cts.Token); + Assert.True(await enumerator.MoveNextAsync()); + Assert.Equal(SmallList[0], enumerator.Current); + cts.Cancel(); + + await Assert.ThrowsAsync(async () => await enumerator.MoveNextAsync()); + } + + [Fact] + public async Task WithPrefetchAsync_ZeroCount() + { + IAsyncEnumerable asyncEnum = SmallList.AsAsyncEnumerable(); + Assert.Same(asyncEnum, await asyncEnum.WithPrefetchAsync(0, this.TimeoutToken)); + } + + [Fact] + public async Task WithPrefetchAsync_NegativeCount() + { + IAsyncEnumerable asyncEnum = SmallList.AsAsyncEnumerable(); + await Assert.ThrowsAsync("count", async () => await asyncEnum.WithPrefetchAsync(-1)); + } + + [Fact] + public async Task WithPrefetchAsync_Twice() + { + var prefetchEnum = await SmallList.AsAsyncEnumerable().WithPrefetchAsync(2, this.TimeoutToken); + await Assert.ThrowsAsync(async () => await prefetchEnum.WithPrefetchAsync(2, this.TimeoutToken)); + } + + [Fact] + public async Task WithPrefetchAsync() + { + await AssertExpectedValues(await SmallList.AsAsyncEnumerable().WithPrefetchAsync(2, this.TimeoutToken)); + } + + [Fact] + public void WithJsonRpcSettings_NullInput() + { + Assert.Throws("enumerable", () => JsonRpcExtensions.WithJsonRpcSettings(null!, null)); + } + + [Fact] + public void WithJsonRpcSettings_NullSettings() + { + var asyncEnum = SmallList.AsAsyncEnumerable(); + Assert.Same(asyncEnum, asyncEnum.WithJsonRpcSettings(null)); + } + + [Fact] + public void WithJsonRpcSettings_GetAsyncEnumerator_Twice() + { + var asyncEnum = SmallList.AsAsyncEnumerable(); + var decorated = asyncEnum.WithJsonRpcSettings(new JsonRpcEnumerableSettings { MinBatchSize = 3 }); + decorated.GetAsyncEnumerator(); + Assert.Throws(() => decorated.GetAsyncEnumerator()); + } + + [Fact] + public async Task WithJsonRpcSettings_GetAsyncEnumerator_Prefetch() + { + var asyncEnum = SmallList.AsAsyncEnumerable(); + var decorated = asyncEnum.WithJsonRpcSettings(new JsonRpcEnumerableSettings { MinBatchSize = 3 }); + decorated.GetAsyncEnumerator(); + await Assert.ThrowsAsync(async () => await decorated.WithPrefetchAsync(2)); + } + + [Fact] + public async Task WithPrefetch_GetAsyncEnumerator_Twice() + { + var asyncEnum = SmallList.AsAsyncEnumerable(); + var decorated = await asyncEnum.WithPrefetchAsync(2, this.TimeoutToken); + decorated.GetAsyncEnumerator(); + Assert.Throws(() => decorated.GetAsyncEnumerator()); + } + + [Fact] + public async Task WithPrefetch_GetAsyncEnumerator_WithPrefetch() + { + var asyncEnum = SmallList.AsAsyncEnumerable(); + var decorated = await asyncEnum.WithPrefetchAsync(2, this.TimeoutToken); + decorated.GetAsyncEnumerator(); + await Assert.ThrowsAsync(async () => await decorated.WithPrefetchAsync(2)); + } + + [Fact] + public void WithJsonRpcSettings_ModifySettings() + { + var asyncEnum = SmallList.AsAsyncEnumerable(); + var decorated = asyncEnum.WithJsonRpcSettings(new JsonRpcEnumerableSettings { MinBatchSize = 3 }); + Assert.NotSame(asyncEnum, decorated); + + // This shouldn't need to rewrap the underlying enumerator or double-decorate. + // Rather, it should just modify the existing decorator. + Assert.Same(decorated, decorated.WithJsonRpcSettings(new JsonRpcEnumerableSettings { MaxReadAhead = 5 })); + } + + private static async Task AssertExpectedValues(IAsyncEnumerable actual) + { int count = 0; - await foreach (int item in collection.AsAsyncEnumerable(new JsonRpcEnumerableSettings { MinBatchSize = 3 })) + await foreach (int item in actual) { Assert.Equal(++count, item); } diff --git a/src/StreamJsonRpc/JsonMessageFormatter.cs b/src/StreamJsonRpc/JsonMessageFormatter.cs index 1e883630..6fb04d21 100644 --- a/src/StreamJsonRpc/JsonMessageFormatter.cs +++ b/src/StreamJsonRpc/JsonMessageFormatter.cs @@ -12,6 +12,7 @@ namespace StreamJsonRpc using System.IO.Pipelines; using System.Linq; using System.Reflection; + using System.Runtime.ExceptionServices; using System.Runtime.Serialization; using System.Text; using System.Threading; @@ -401,6 +402,11 @@ public JToken Serialize(JsonRpcMessage message) } /// + /// + /// Do *NOT* change this to simply forward to since that + /// mutates the itself by tokenizing arguments as if we were going to transmit them + /// which BREAKS argument parsing for incoming named argument messages such as $/cancelRequest. + /// public object GetJsonText(JsonRpcMessage message) => JToken.FromObject(message); /// @@ -875,6 +881,8 @@ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer /// private class AsyncEnumerableConsumerConverter : JsonConverter { + private static readonly MethodInfo ReadJsonOpenGenericMethod = typeof(AsyncEnumerableConsumerConverter).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic).Single(m => m.Name == nameof(ReadJson) && m.IsGenericMethod); + private JsonMessageFormatter formatter; internal AsyncEnumerableConsumerConverter(JsonMessageFormatter jsonMessageFormatter) @@ -891,14 +899,33 @@ internal AsyncEnumerableConsumerConverter(JsonMessageFormatter jsonMessageFormat return null; } - JToken token = JToken.Load(reader); - return this.formatter.EnumerableTracker.CreateEnumerableProxy(objectType, token); + Type? iface = TrackerHelpers>.FindInterfaceImplementedBy(objectType); + Assumes.NotNull(iface); + MethodInfo genericMethod = ReadJsonOpenGenericMethod.MakeGenericMethod(iface.GenericTypeArguments[0]); + try + { + return genericMethod.Invoke(this, new object?[] { reader, serializer }); + } + catch (TargetInvocationException ex) + { + ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); + throw Assumes.NotReachable(); + } } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { throw new NotSupportedException(); } + + private IAsyncEnumerable ReadJson(JsonReader reader, JsonSerializer serializer) + { + JToken enumJToken = JToken.Load(reader); + JToken handle = enumJToken[MessageFormatterEnumerableTracker.TokenPropertyName]; + IReadOnlyList? prefetchedItems = enumJToken[MessageFormatterEnumerableTracker.ValuesPropertyName]?.ToObject>(serializer); + + return this.formatter.EnumerableTracker.CreateEnumerableProxy(handle, prefetchedItems); + } } /// @@ -906,6 +933,8 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s /// private class AsyncEnumerableGeneratorConverter : JsonConverter { + private static readonly MethodInfo WriteJsonOpenGenericMethod = typeof(AsyncEnumerableGeneratorConverter).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).Single(m => m.Name == nameof(WriteJson) && m.IsGenericMethod); + private JsonMessageFormatter formatter; internal AsyncEnumerableGeneratorConverter(JsonMessageFormatter jsonMessageFormatter) @@ -922,8 +951,39 @@ internal AsyncEnumerableGeneratorConverter(JsonMessageFormatter jsonMessageForma public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { + Type? iface = TrackerHelpers>.FindInterfaceImplementedBy(value.GetType()); + Assumes.NotNull(iface); + MethodInfo genericMethod = WriteJsonOpenGenericMethod.MakeGenericMethod(iface.GenericTypeArguments[0]); + try + { + genericMethod.Invoke(this, new object?[] { writer, value, serializer }); + } + catch (TargetInvocationException ex) + { + ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); + throw Assumes.NotReachable(); + } + } + + private void WriteJson(JsonWriter writer, IAsyncEnumerable value, JsonSerializer serializer) + { + (IReadOnlyList Elements, bool Finished) prefetched = value.TearOffPrefetchedElements(); long token = this.formatter.EnumerableTracker.GetToken(value); - writer.WriteValue(token); + writer.WriteStartObject(); + + if (!prefetched.Finished) + { + writer.WritePropertyName(MessageFormatterEnumerableTracker.TokenPropertyName); + writer.WriteValue(token); + } + + if (prefetched.Elements.Count > 0) + { + writer.WritePropertyName(MessageFormatterEnumerableTracker.ValuesPropertyName); + serializer.Serialize(writer, prefetched.Elements); + } + + writer.WriteEndObject(); } } diff --git a/src/StreamJsonRpc/JsonRpcExtensions.cs b/src/StreamJsonRpc/JsonRpcExtensions.cs index 401f9f5b..13a2ed68 100644 --- a/src/StreamJsonRpc/JsonRpcExtensions.cs +++ b/src/StreamJsonRpc/JsonRpcExtensions.cs @@ -3,8 +3,11 @@ namespace StreamJsonRpc { + using System; using System.Collections.Generic; + using System.Runtime.CompilerServices; using System.Threading; + using System.Threading.Tasks; using Microsoft; /// @@ -23,26 +26,44 @@ public static IAsyncEnumerable WithJsonRpcSettings(this IAsyncEnumerable(enumerable, settings) : enumerable; + if (settings == null) + { + return enumerable; + } + + RpcEnumerable rpcEnumerable = GetRpcEnumerable(enumerable); + rpcEnumerable.Settings = settings; + return rpcEnumerable; } + /// + public static IAsyncEnumerable AsAsyncEnumerable(this IEnumerable enumerable) => AsAsyncEnumerable(enumerable, CancellationToken.None); + /// /// Converts an to so it will be streamed over an RPC connection progressively /// instead of as an entire collection in one message. /// /// The type of element enumerated by the sequence. /// The enumerable to be converted. + /// A cancellation token. /// The async enumerable instance. #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - public static async IAsyncEnumerable AsAsyncEnumerable(this IEnumerable enumerable) + public static async IAsyncEnumerable AsAsyncEnumerable(this IEnumerable enumerable, [EnumeratorCancellation] CancellationToken cancellationToken) #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously { + Requires.NotNull(enumerable, nameof(enumerable)); + + cancellationToken.ThrowIfCancellationRequested(); foreach (T item in enumerable) { yield return item; + cancellationToken.ThrowIfCancellationRequested(); } } + /// + public static IAsyncEnumerable AsAsyncEnumerable(this IEnumerable enumerable, JsonRpcEnumerableSettings? settings) => AsAsyncEnumerable(enumerable, settings, CancellationToken.None); + /// /// Converts an to so it will be streamed over an RPC connection progressively /// instead of as an entire collection in one message. @@ -50,10 +71,34 @@ public static async IAsyncEnumerable AsAsyncEnumerable(this IEnumerable /// The type of element enumerated by the sequence. /// The enumerable to be converted. /// The settings to associate with this enumerable. + /// A cancellation token. /// The async enumerable instance. - public static IAsyncEnumerable AsAsyncEnumerable(this IEnumerable enumerable, JsonRpcEnumerableSettings? settings) + public static IAsyncEnumerable AsAsyncEnumerable(this IEnumerable enumerable, JsonRpcEnumerableSettings? settings, CancellationToken cancellationToken) { - return AsAsyncEnumerable(enumerable).WithJsonRpcSettings(settings); + return AsAsyncEnumerable(enumerable, cancellationToken).WithJsonRpcSettings(settings); + } + + /// + /// Preloads an with a cache of pre-enumerated items for inclusion in the initial transmission + /// of the enumerable over an RPC channel. + /// + /// The type of item in the collection. + /// The sequence to pre-fetch items from. + /// The number of items to pre-fetch. If this value is larger than the number of elements in the enumerable, all values will be pre-fetched. + /// A cancellation token. + /// A decorated object that is specially prepared for processing by JSON-RPC with the preloaded values. + public static async ValueTask> WithPrefetchAsync(this IAsyncEnumerable enumerable, int count, CancellationToken cancellationToken = default) + { + Requires.NotNull(enumerable, nameof(enumerable)); + + if (count == 0) + { + return enumerable; + } + + RpcEnumerable rpcEnumerable = GetRpcEnumerable(enumerable); + await rpcEnumerable.PrefetchAsync(count, cancellationToken).ConfigureAwait(false); + return rpcEnumerable; } /// @@ -71,22 +116,117 @@ internal static JsonRpcEnumerableSettings GetJsonRpcSettings(this IAsyncEnume { Requires.NotNull(enumerable, nameof(enumerable)); - return enumerable is SettingsBearingEnumerable decorated ? decorated.Settings : JsonRpcEnumerableSettings.DefaultSettings; + return (enumerable as RpcEnumerable)?.Settings ?? JsonRpcEnumerableSettings.DefaultSettings; + } + + internal static (IReadOnlyList Elements, bool Finished) TearOffPrefetchedElements(this IAsyncEnumerable enumerable) + { + Requires.NotNull(enumerable, nameof(enumerable)); + + return (enumerable as RpcEnumerable)?.TearOffPrefetchedElements() ?? (Array.Empty(), false); } - internal class SettingsBearingEnumerable : IAsyncEnumerable + private static RpcEnumerable GetRpcEnumerable(IAsyncEnumerable enumerable) => enumerable as RpcEnumerable ?? new RpcEnumerable(enumerable); + + private class RpcEnumerable : IAsyncEnumerable { - private readonly IAsyncEnumerable innerEnumerable; + private readonly RpcEnumerator innerEnumerator; + + private bool enumeratorRequested; + + internal RpcEnumerable(IAsyncEnumerable enumerable) + { + this.innerEnumerator = new RpcEnumerator(enumerable); + } + + internal JsonRpcEnumerableSettings Settings { get; set; } = JsonRpcEnumerableSettings.DefaultSettings; + + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken) + { + Verify.Operation(!this.enumeratorRequested, Resources.CannotBeCalledAfterGetAsyncEnumerator); + this.enumeratorRequested = true; + return this.innerEnumerator; + } + + internal (IReadOnlyList Elements, bool Finished) TearOffPrefetchedElements() => this.innerEnumerator.TearOffPrefetchedElements(); - internal SettingsBearingEnumerable(IAsyncEnumerable enumerable, JsonRpcEnumerableSettings settings) + internal Task PrefetchAsync(int count, CancellationToken cancellationToken) { - this.innerEnumerable = enumerable; - this.Settings = settings; + Verify.Operation(!this.enumeratorRequested, Resources.CannotBeCalledAfterGetAsyncEnumerator); + return this.innerEnumerator.PrefetchAsync(count, cancellationToken); } - internal JsonRpcEnumerableSettings Settings { get; } + private class RpcEnumerator : IAsyncEnumerator + { + private readonly IAsyncEnumerator innerEnumerator; + + private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + + private List? prefetchedElements; + + private bool finished; + + private bool moveNextHasBeenCalled; + + internal RpcEnumerator(IAsyncEnumerable enumerable) + { + this.innerEnumerator = enumerable.GetAsyncEnumerator(this.cancellationTokenSource.Token); + } - public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken) => this.innerEnumerable.GetAsyncEnumerator(cancellationToken); + public T Current => this.moveNextHasBeenCalled && this.prefetchedElements?.Count > 0 ? this.prefetchedElements[0] : this.innerEnumerator.Current; + + public ValueTask DisposeAsync() + { + return this.innerEnumerator.DisposeAsync(); + } + + public ValueTask MoveNextAsync() + { + bool moveNextHasBeenCalledBefore = this.moveNextHasBeenCalled; + this.moveNextHasBeenCalled = true; + + if (this.prefetchedElements?.Count > 0 && moveNextHasBeenCalledBefore) + { + this.prefetchedElements.RemoveAt(0); + } + + if (this.prefetchedElements?.Count > 0) + { + return new ValueTask(true); + } + + return this.innerEnumerator.MoveNextAsync(); + } + + internal (IReadOnlyList Elements, bool Finished) TearOffPrefetchedElements() + { + Assumes.False(this.moveNextHasBeenCalled); + IReadOnlyList result = (IReadOnlyList?)this.prefetchedElements ?? Array.Empty(); + this.prefetchedElements = null; + return (result, this.finished); + } + + internal async Task PrefetchAsync(int count, CancellationToken cancellationToken) + { + Requires.Range(count >= 0, nameof(count)); + Verify.Operation(this.prefetchedElements == null, Resources.ElementsAlreadyPrefetched); + + // Arrange to cancel the entire enumerator if the prefetch is canceled. + using CancellationTokenRegistration ctr = this.LinkToCancellation(cancellationToken); + + var prefetchedElements = new List(count); + bool moreAvailable = true; + for (int i = 0; i < count && (moreAvailable = await this.innerEnumerator.MoveNextAsync().ConfigureAwait(false)); i++) + { + prefetchedElements.Add(this.innerEnumerator.Current); + } + + this.finished = !moreAvailable; + this.prefetchedElements = prefetchedElements; + } + + private CancellationTokenRegistration LinkToCancellation(CancellationToken cancellationToken) => cancellationToken.Register(cts => ((CancellationTokenSource)cts).Cancel(), this.cancellationTokenSource); + } } } } diff --git a/src/StreamJsonRpc/MessagePackFormatter.cs b/src/StreamJsonRpc/MessagePackFormatter.cs index 865f7fde..304abb73 100644 --- a/src/StreamJsonRpc/MessagePackFormatter.cs +++ b/src/StreamJsonRpc/MessagePackFormatter.cs @@ -417,6 +417,8 @@ private RawMessagePack(ReadOnlyMemory raw) this.rawMemory = raw; } + internal bool IsDefault => this.rawMemory.IsEmpty && this.rawSequence.IsEmpty; + /// /// Reads one raw messagepack token. /// @@ -653,23 +655,68 @@ public PreciseTypeFormatter(MessagePackFormatter mainFormatter) return default; } - Assumes.NotNull(this.mainFormatter.enumerableTracker); - RawMessagePack token = RawMessagePack.ReadRaw(ref reader, copy: true); - return this.mainFormatter.enumerableTracker.CreateEnumerableProxy(token); + RawMessagePack token = default; + IReadOnlyList? initialElements = null; + int propertyCount = reader.ReadMapHeader(); + for (int i = 0; i < propertyCount; i++) + { + switch (reader.ReadString()) + { + case MessageFormatterEnumerableTracker.TokenPropertyName: + token = RawMessagePack.ReadRaw(ref reader, copy: true); + break; + case MessageFormatterEnumerableTracker.ValuesPropertyName: + initialElements = options.Resolver.GetFormatterWithVerify>().Deserialize(ref reader, options); + break; + } + } + + return this.mainFormatter.EnumerableTracker.CreateEnumerableProxy(token.IsDefault ? null : (object)token, initialElements); } public void Serialize(ref MessagePackWriter writer, IAsyncEnumerable? value, MessagePackSerializerOptions options) { - Assumes.NotNull(this.mainFormatter.enumerableTracker); + Serialize_Shared(this.mainFormatter, ref writer, value, options); + } + + internal static MessagePackWriter Serialize_Shared(MessagePackFormatter mainFormatter, ref MessagePackWriter writer, IAsyncEnumerable? value, MessagePackSerializerOptions options) + { if (value is null) { writer.WriteNil(); } else { - long token = this.mainFormatter.enumerableTracker.GetToken(value); - writer.Write(token); + (IReadOnlyList Elements, bool Finished) prefetched = value.TearOffPrefetchedElements(); + long token = mainFormatter.EnumerableTracker.GetToken(value); + + int propertyCount = 0; + if (prefetched.Elements.Count > 0) + { + propertyCount++; + } + + if (!prefetched.Finished) + { + propertyCount++; + } + + writer.WriteMapHeader(propertyCount); + + if (!prefetched.Finished) + { + writer.Write(MessageFormatterEnumerableTracker.TokenPropertyName); + writer.Write(token); + } + + if (prefetched.Elements.Count > 0) + { + writer.Write(MessageFormatterEnumerableTracker.ValuesPropertyName); + options.Resolver.GetFormatterWithVerify>().Serialize(ref writer, prefetched.Elements, options); + } } + + return writer; } } @@ -693,16 +740,7 @@ public TClass Deserialize(ref MessagePackReader reader, MessagePackSerializerOpt public void Serialize(ref MessagePackWriter writer, TClass value, MessagePackSerializerOptions options) { - Assumes.NotNull(this.mainFormatter.enumerableTracker); - if (value is null) - { - writer.WriteNil(); - } - else - { - long token = this.mainFormatter.enumerableTracker.GetToken(value); - writer.Write(token); - } + writer = PreciseTypeFormatter.Serialize_Shared(this.mainFormatter, ref writer, value, options); } } } diff --git a/src/StreamJsonRpc/ProxyGeneration.cs b/src/StreamJsonRpc/ProxyGeneration.cs index 2bacfbfd..f063c335 100644 --- a/src/StreamJsonRpc/ProxyGeneration.cs +++ b/src/StreamJsonRpc/ProxyGeneration.cs @@ -303,7 +303,7 @@ internal static TypeInfo Get(TypeInfo serviceInterface) il.EmitCall(OpCodes.Callvirt, invokingMethod, null); - AdaptReturnType(method, returnTypeIsValueTask, returnTypeIsIAsyncEnumerable, il, invokingMethod); + AdaptReturnType(method, returnTypeIsValueTask, returnTypeIsIAsyncEnumerable, il, invokingMethod, cancellationTokenParameter); il.Emit(OpCodes.Ret); } @@ -346,7 +346,7 @@ internal static TypeInfo Get(TypeInfo serviceInterface) il.EmitCall(OpCodes.Callvirt, invokingMethod, null); - AdaptReturnType(method, returnTypeIsValueTask, returnTypeIsIAsyncEnumerable, il, invokingMethod); + AdaptReturnType(method, returnTypeIsValueTask, returnTypeIsIAsyncEnumerable, il, invokingMethod, cancellationTokenParameter); il.Emit(OpCodes.Ret); } @@ -374,7 +374,8 @@ internal static TypeInfo Get(TypeInfo serviceInterface) /// true if the return type is ; false otherwise. /// The IL emitter for the method. /// The Invoke method on that IL was just emitted to invoke. - private static void AdaptReturnType(MethodInfo method, bool returnTypeIsValueTask, bool returnTypeIsIAsyncEnumerable, ILGenerator il, MethodInfo invokingMethod) + /// The parameter in the proxy method, if there is one. + private static void AdaptReturnType(MethodInfo method, bool returnTypeIsValueTask, bool returnTypeIsIAsyncEnumerable, ILGenerator il, MethodInfo invokingMethod, ParameterInfo? cancellationTokenParameter) { if (returnTypeIsValueTask) { @@ -384,6 +385,19 @@ private static void AdaptReturnType(MethodInfo method, bool returnTypeIsValueTas else if (returnTypeIsIAsyncEnumerable) { // We must convert the Task> to IAsyncEnumerable + // Push a CancellationToken to the stack as well. Use the one this method was given if available, otherwise push CancellationToken.None. + if (cancellationTokenParameter != null) + { + il.Emit(OpCodes.Ldarg, cancellationTokenParameter.Position + 1); + } + else + { + LocalBuilder local = il.DeclareLocal(typeof(CancellationToken)); + il.Emit(OpCodes.Ldloca, local); + il.Emit(OpCodes.Initobj, typeof(CancellationToken)); + il.Emit(OpCodes.Ldloc, local); + } + Type proxyEnumerableType = typeof(AsyncEnumerableProxy<>).MakeGenericType(method.ReturnType.GenericTypeArguments[0]); ConstructorInfo ctor = proxyEnumerableType.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance).Single(); il.Emit(OpCodes.Newobj, ctor); @@ -596,15 +610,17 @@ private static IEnumerable FindAllOnThisAndOtherInterfaces(TypeInfo interf private class AsyncEnumerableProxy : IAsyncEnumerable { private readonly Task> enumerableTask; + private readonly CancellationToken defaultCancellationToken; - internal AsyncEnumerableProxy(Task> enumerableTask) + internal AsyncEnumerableProxy(Task> enumerableTask, CancellationToken defaultCancellationToken) { this.enumerableTask = enumerableTask ?? throw new ArgumentNullException(nameof(enumerableTask)); + this.defaultCancellationToken = defaultCancellationToken; } public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken) { - return new AsyncEnumeratorProxy(this.enumerableTask, cancellationToken); + return new AsyncEnumeratorProxy(this.enumerableTask, cancellationToken.CanBeCanceled ? cancellationToken : this.defaultCancellationToken); } private class AsyncEnumeratorProxy : IAsyncEnumerator diff --git a/src/StreamJsonRpc/Reflection/MessageFormatterEnumerableTracker.cs b/src/StreamJsonRpc/Reflection/MessageFormatterEnumerableTracker.cs index 9db91a68..cfbaae9b 100644 --- a/src/StreamJsonRpc/Reflection/MessageFormatterEnumerableTracker.cs +++ b/src/StreamJsonRpc/Reflection/MessageFormatterEnumerableTracker.cs @@ -25,11 +25,23 @@ namespace StreamJsonRpc.Reflection /// public class MessageFormatterEnumerableTracker { - private const string DisposeMethodName = "$/enumerator/dispose"; - private const string NextMethodName = "$/enumerator/next"; + /// + /// The name of the string property that carries the handle for the enumerable. + /// + public const string TokenPropertyName = "token"; + + /// + /// The name of the JSON array property that contains the values. + /// + public const string ValuesPropertyName = "values"; + + /// + /// The name of the boolean property that indicates whether the last value has been returned to the consumer. + /// + private const string FinishedPropertyName = "finished"; - private static readonly MethodInfo GetTokenOpenGenericMethod = typeof(MessageFormatterEnumerableTracker).GetRuntimeMethods().First(m => m.Name == nameof(GetToken) && m.IsGenericMethod); - private static readonly MethodInfo CreateEnumerableProxyOpenGenericMethod = typeof(MessageFormatterEnumerableTracker).GetRuntimeMethods().First(m => m.Name == nameof(CreateEnumerableProxy) && m.IsGenericMethod); + private const string NextMethodName = "$/enumerator/next"; + private const string DisposeMethodName = "$/enumerator/abort"; /// /// Dictionary used to map the outbound request id to their progress info so that the progress objects are cleaned after getting the final response. @@ -69,11 +81,6 @@ public MessageFormatterEnumerableTracker(JsonRpc jsonRpc, IJsonRpcFormatterState private interface IGeneratingEnumeratorTracker : System.IAsyncDisposable { - /// - /// Gets a value indicating whether the consumer has actually acknowledged the enumerable by requesting the first value. - /// - bool HasEnumerationStarted { get; } - ValueTask GetNextValuesAsync(CancellationToken cancellationToken); } @@ -120,62 +127,23 @@ public long GetToken(IAsyncEnumerable enumerable) } this.generatorTokensByRequestId[this.formatterState.SerializingMessageWithId] = tokens.Add(handle); - this.generatorsByToken.Add(handle, new GeneratingEnumeratorTracker(enumerable, settings: enumerable.GetJsonRpcSettings())); + this.generatorsByToken.Add(handle, new GeneratingEnumeratorTracker(this, handle, enumerable, settings: enumerable.GetJsonRpcSettings())); } return handle; } - /// - /// Used by the generator to assign a handle to the given . - /// - /// The enumerable to assign a handle to. - /// The handle that was assigned. - public long GetToken(object enumerable) - { - Requires.NotNull(enumerable, nameof(enumerable)); - Type? iface = TrackerHelpers>.FindInterfaceImplementedBy(enumerable.GetType()); - Requires.Argument(iface != null, nameof(enumerable), message: null); - - MethodInfo closedGenericMethod = GetTokenOpenGenericMethod.MakeGenericMethod(iface.GenericTypeArguments[0]); - try - { - return (long)closedGenericMethod.Invoke(this, new object[] { enumerable }); - } - catch (TargetInvocationException ex) - { - ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); - throw Assumes.NotReachable(); - } - } - /// /// Used by the consumer to construct a proxy that implements /// and gets all its values from a remote generator. /// /// The type of value that is produced by the enumerable. - /// The handle specified by the generator that is used to obtain more values or dispose of the enumerator. - /// The enumerator. - public IAsyncEnumerable CreateEnumerableProxy(object handle) - { - return new AsyncEnumerableProxy(this.jsonRpc, handle); - } - - /// - /// Used by the consumer to construct a proxy that implements - /// and gets all its values from a remote generator. - /// - /// The type of value that is produced by the enumerable. - /// The handle specified by the generator that is used to obtain more values or dispose of the enumerator. + /// The handle specified by the generator that is used to obtain more values or dispose of the enumerator. May be null to indicate there will be no more values. + /// The list of items that are included with the enumerable handle. /// The enumerator. - public object CreateEnumerableProxy(Type enumeratedType, object handle) + public IAsyncEnumerable CreateEnumerableProxy(object? handle, IReadOnlyList? prefetchedItems) { - Requires.NotNull(enumeratedType, nameof(enumeratedType)); - Requires.NotNull(handle, nameof(handle)); - - Requires.Argument(CanDeserialize(enumeratedType), nameof(enumeratedType), message: null); - MethodInfo closedGenericMethod = CreateEnumerableProxyOpenGenericMethod.MakeGenericMethod(enumeratedType.GenericTypeArguments[0]); - return closedGenericMethod.Invoke(this, new object[] { handle }); + return new AsyncEnumerableProxy(this.jsonRpc, handle, prefetchedItems); } private ValueTask OnNextAsync(long token, CancellationToken cancellationToken) @@ -199,7 +167,7 @@ private ValueTask OnDisposeAsync(long token) { if (!this.generatorsByToken.TryGetValue(token, out generator)) { - return default; + throw new LocalRpcException(Resources.UnknownTokenToMarshaledObject) { ErrorCode = (int)JsonRpcErrorCode.NoMarshaledObjectFound }; } this.generatorsByToken.Remove(token); @@ -230,104 +198,123 @@ private class GeneratingEnumeratorTracker : IGeneratingEnumeratorTracker private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); - private readonly BufferBlock? prefetchedElements; + private readonly BufferBlock? readAheadElements; + + private readonly MessageFormatterEnumerableTracker tracker; + + private readonly long token; - internal GeneratingEnumeratorTracker(IAsyncEnumerable enumerable, JsonRpcEnumerableSettings settings) + internal GeneratingEnumeratorTracker(MessageFormatterEnumerableTracker tracker, long token, IAsyncEnumerable enumerable, JsonRpcEnumerableSettings settings) { + this.tracker = tracker; + this.token = token; this.enumerator = enumerable.GetAsyncEnumerator(this.cancellationTokenSource.Token); this.Settings = settings; if (settings.MaxReadAhead > 0) { - this.prefetchedElements = new BufferBlock(new DataflowBlockOptions { BoundedCapacity = settings.MaxReadAhead, EnsureOrdered = true }); - this.PrefetchAsync().Forget(); // exceptions fault the buffer block + this.readAheadElements = new BufferBlock(new DataflowBlockOptions { BoundedCapacity = settings.MaxReadAhead, EnsureOrdered = true }); + this.ReadAheadAsync().Forget(); // exceptions fault the buffer block } } - public bool HasEnumerationStarted { get; private set; } - internal JsonRpcEnumerableSettings Settings { get; } public async ValueTask GetNextValuesAsync(CancellationToken cancellationToken) { - this.HasEnumerationStarted = true; - using (cancellationToken.Register(state => ((CancellationTokenSource)state).Cancel(), this.cancellationTokenSource)) + try { - cancellationToken = this.cancellationTokenSource.Token; - bool finished = false; - var results = new List(this.Settings.MinBatchSize); - if (this.prefetchedElements != null) + using (cancellationToken.Register(state => ((CancellationTokenSource)state).Cancel(), this.cancellationTokenSource)) { - // Fetch at least the min batch size and at most the number that has been cached up to this point (or until we hit the end of the sequence). - // We snap the number of cached elements up front because as we dequeue, we create capacity to store more and we don't want to - // collect and return more than MaxReadAhead. - int cachedOnEntry = this.prefetchedElements.Count; - for (int i = 0; !this.prefetchedElements.Completion.IsCompleted && (i < this.Settings.MinBatchSize || (cachedOnEntry - results.Count > 0)); i++) + cancellationToken = this.cancellationTokenSource.Token; + bool finished = false; + var results = new List(this.Settings.MinBatchSize); + if (this.readAheadElements != null) { - try + // Fetch at least the min batch size and at most the number that has been cached up to this point (or until we hit the end of the sequence). + // We snap the number of cached elements up front because as we dequeue, we create capacity to store more and we don't want to + // collect and return more than MaxReadAhead. + int cachedOnEntry = this.readAheadElements.Count; + for (int i = 0; !this.readAheadElements.Completion.IsCompleted && (i < this.Settings.MinBatchSize || (cachedOnEntry - results.Count > 0)); i++) { - T element = await this.prefetchedElements.ReceiveAsync(cancellationToken).ConfigureAwait(false); - results.Add(element); + try + { + T element = await this.readAheadElements.ReceiveAsync(cancellationToken).ConfigureAwait(false); + results.Add(element); + } + catch (InvalidOperationException) when (this.readAheadElements.Completion.IsCompleted) + { + // Race condition. The sequence is over. + finished = true; + break; + } } - catch (InvalidOperationException) when (this.prefetchedElements.Completion.IsCompleted) + + if (this.readAheadElements.Completion.IsCompleted) { - // Race condition. The sequence is over. + // Rethrow any exceptions. + await this.readAheadElements.Completion.ConfigureAwait(false); finished = true; - break; } } - - if (this.prefetchedElements.Completion.IsCompleted) + else { - // Rethrow any exceptions. - await this.prefetchedElements.Completion.ConfigureAwait(false); - finished = true; - } - } - else - { - for (int i = 0; i < this.Settings.MinBatchSize; i++) - { - if (!await this.enumerator.MoveNextAsync().ConfigureAwait(false)) + for (int i = 0; i < this.Settings.MinBatchSize; i++) { - finished = true; - break; + if (!await this.enumerator.MoveNextAsync().ConfigureAwait(false)) + { + finished = true; + break; + } + + results.Add(this.enumerator.Current); } + } - results.Add(this.enumerator.Current); + if (finished) + { + // Clean up all resources since we don't expect the client to send a dispose notification + // since finishing the enumeration implicitly should dispose of it. + await this.tracker.OnDisposeAsync(this.token).ConfigureAwait(false); } - } - return new EnumeratorResults - { - Finished = finished, - Values = results, - }; + return new EnumeratorResults + { + Finished = finished, + Values = results, + }; + } + } + catch + { + // An error is considered fatal to the enumerable, so clean up everything. + await this.tracker.OnDisposeAsync(this.token).ConfigureAwait(false); + throw; } } public ValueTask DisposeAsync() { this.cancellationTokenSource.Cancel(); - this.prefetchedElements?.Complete(); + this.readAheadElements?.Complete(); return this.enumerator.DisposeAsync(); } - private async Task PrefetchAsync() + private async Task ReadAheadAsync() { - Assumes.NotNull(this.prefetchedElements); + Assumes.NotNull(this.readAheadElements); try { while (await this.enumerator.MoveNextAsync().ConfigureAwait(false)) { - await this.prefetchedElements.SendAsync(this.enumerator.Current, this.cancellationTokenSource.Token).ConfigureAwait(false); + await this.readAheadElements.SendAsync(this.enumerator.Current, this.cancellationTokenSource.Token).ConfigureAwait(false); } - this.prefetchedElements.Complete(); + this.readAheadElements.Complete(); } catch (Exception ex) { - ITargetBlock target = this.prefetchedElements; + ITargetBlock target = this.readAheadElements; target.Fault(ex); } } @@ -340,130 +327,159 @@ private async Task PrefetchAsync() private class AsyncEnumerableProxy : IAsyncEnumerable { private readonly JsonRpc jsonRpc; + private readonly bool finished; private object? handle; + private bool enumeratorAcquired; + private IReadOnlyList? prefetchedItems; - internal AsyncEnumerableProxy(JsonRpc jsonRpc, object handle) + internal AsyncEnumerableProxy(JsonRpc jsonRpc, object? handle, IReadOnlyList? prefetchedItems) { this.jsonRpc = jsonRpc; this.handle = handle; + this.prefetchedItems = prefetchedItems; + this.finished = handle == null; } public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken) { - object handle = this.handle ?? throw new InvalidOperationException(Resources.UsableOnceOnly); - this.handle = null; - return new AsyncEnumeratorProxy(this.jsonRpc, handle, cancellationToken); + Verify.Operation(!this.enumeratorAcquired, Resources.CannotBeCalledAfterGetAsyncEnumerator); + this.enumeratorAcquired = true; + var result = new AsyncEnumeratorProxy(this, this.handle, this.prefetchedItems, this.finished, cancellationToken); + this.prefetchedItems = null; + return result; } - } - - /// - /// Provides the instance that is used by a consumer. - /// - /// The type of value produced by the enumerator. - private class AsyncEnumeratorProxy : IAsyncEnumerator - { - private readonly JsonRpc jsonRpc; - private readonly CancellationToken cancellationToken; - private readonly object?[] nextOrDisposeArguments; /// - /// A sequence of values that have already been received from the generator but not yet consumed. + /// Provides the instance that is used by a consumer. /// - private Sequence localCachedValues = new Sequence(); + private class AsyncEnumeratorProxy : IAsyncEnumerator + { + private readonly AsyncEnumerableProxy owner; + private readonly CancellationToken cancellationToken; + private readonly object[]? nextOrDisposeArguments; - /// - /// A value indicating whether the generator has reported that no more values will be forthcoming. - /// - private bool generatorReportsFinished; + /// + /// A sequence of values that have already been received from the generator but not yet consumed. + /// + private Sequence localCachedValues = new Sequence(); - private bool disposed; + /// + /// A value indicating whether the generator has reported that no more values will be forthcoming. + /// + private bool generatorReportsFinished; - internal AsyncEnumeratorProxy(JsonRpc jsonRpc, object handle, CancellationToken cancellationToken) - { - this.jsonRpc = jsonRpc; - this.nextOrDisposeArguments = new object?[] { handle }; - this.cancellationToken = cancellationToken; - } + private bool moveNextCalled; - public T Current - { - get + private bool disposed; + + internal AsyncEnumeratorProxy(AsyncEnumerableProxy owner, object? handle, IReadOnlyList? prefetchedItems, bool finished, CancellationToken cancellationToken) { - Verify.NotDisposed(!this.disposed, this); - if (this.localCachedValues.Length == 0) + this.owner = owner; + this.nextOrDisposeArguments = handle != null ? new object[] { handle } : null; + this.cancellationToken = cancellationToken; + + if (prefetchedItems != null) { - throw new InvalidOperationException("Call " + nameof(this.MoveNextAsync) + " first and confirm it returns true first."); + Write(this.localCachedValues, prefetchedItems); } - return this.localCachedValues.AsReadOnlySequence.First.Span[0]; + this.generatorReportsFinished = finished; } - } - public async ValueTask DisposeAsync() - { - if (!this.disposed) + public T Current { - this.disposed = true; - - // Recycle buffers - this.localCachedValues.Reset(); + get + { + Verify.NotDisposed(!this.disposed, this); + if (this.localCachedValues.Length == 0) + { + throw new InvalidOperationException("Call " + nameof(this.MoveNextAsync) + " first and confirm it returns true first."); + } - // Notify server. - await this.jsonRpc.NotifyAsync(DisposeMethodName, this.nextOrDisposeArguments).ConfigureAwait(false); + return this.localCachedValues.AsReadOnlySequence.First.Span[0]; + } } - } - - public async ValueTask MoveNextAsync() - { - Verify.NotDisposed(!this.disposed, this); - // Consume one locally cached value, if we have one. - if (this.localCachedValues.Length > 0) + public async ValueTask DisposeAsync() { - this.localCachedValues.AdvanceTo(this.localCachedValues.AsReadOnlySequence.GetPosition(1)); + if (!this.disposed) + { + this.disposed = true; + + // Recycle buffers + this.localCachedValues.Reset(); + + // Notify server if it wasn't already finished. + if (!this.generatorReportsFinished) + { + await this.owner.jsonRpc.NotifyAsync(DisposeMethodName, this.nextOrDisposeArguments).ConfigureAwait(false); + } + } } - if (this.localCachedValues.Length == 0 && !this.generatorReportsFinished) + public async ValueTask MoveNextAsync() { - // Fetch more values - try + Verify.NotDisposed(!this.disposed, this); + + // Consume one locally cached value, if we have one. + if (this.localCachedValues.Length > 0) { - EnumeratorResults results = await this.jsonRpc.InvokeWithCancellationAsync>(NextMethodName, this.nextOrDisposeArguments, this.cancellationToken).ConfigureAwait(false); - if (results.Values != null) + if (this.moveNextCalled) { - Write(this.localCachedValues, results.Values); + this.localCachedValues.AdvanceTo(this.localCachedValues.AsReadOnlySequence.GetPosition(1)); + } + else + { + // Don't consume one the first time we're called if we have an initial set of values. + this.moveNextCalled = true; + return true; } - - this.generatorReportsFinished = results.Finished; } - catch (RemoteInvocationException ex) when (ex.ErrorCode == (int)JsonRpcErrorCode.NoMarshaledObjectFound) + + this.moveNextCalled = true; + + if (this.localCachedValues.Length == 0 && !this.generatorReportsFinished) { - throw new InvalidOperationException(ex.Message, ex); + // Fetch more values + try + { + EnumeratorResults results = await this.owner.jsonRpc.InvokeWithCancellationAsync>(NextMethodName, this.nextOrDisposeArguments, this.cancellationToken).ConfigureAwait(false); + if (results.Values != null) + { + Write(this.localCachedValues, results.Values); + } + + this.generatorReportsFinished = results.Finished; + } + catch (RemoteInvocationException ex) when (ex.ErrorCode == (int)JsonRpcErrorCode.NoMarshaledObjectFound) + { + throw new InvalidOperationException(ex.Message, ex); + } } - } - return this.localCachedValues.Length > 0; - } + return this.localCachedValues.Length > 0; + } - private static void Write(IBufferWriter writer, IReadOnlyList values) - { - Span span = writer.GetSpan(values.Count); - for (int i = 0; i < values.Count; i++) + private static void Write(IBufferWriter writer, IReadOnlyList values) { - span[i] = values[i]; - } + Span span = writer.GetSpan(values.Count); + for (int i = 0; i < values.Count; i++) + { + span[i] = values[i]; + } - writer.Advance(values.Count); + writer.Advance(values.Count); + } } } [DataContract] private class EnumeratorResults { - [DataMember(Name = "values", Order = 0)] + [DataMember(Name = ValuesPropertyName, Order = 0)] internal IReadOnlyList? Values { get; set; } - [DataMember(Name = "finished", Order = 1)] + [DataMember(Name = FinishedPropertyName, Order = 1)] internal bool Finished { get; set; } } } diff --git a/src/StreamJsonRpc/Resources.Designer.cs b/src/StreamJsonRpc/Resources.Designer.cs index 8f17c74d..ce16f80e 100644 --- a/src/StreamJsonRpc/Resources.Designer.cs +++ b/src/StreamJsonRpc/Resources.Designer.cs @@ -78,6 +78,15 @@ internal static string CancellationTokenMustBeLastParameter { } } + /// + /// Looks up a localized string similar to This cannot be done after GetAsyncEnumerator has already been called.. + /// + internal static string CannotBeCalledAfterGetAsyncEnumerator { + get { + return ResourceManager.GetString("CannotBeCalledAfterGetAsyncEnumerator", resourceCulture); + } + } + /// /// Looks up a localized string similar to "{0}" is not an interface.. /// @@ -132,6 +141,15 @@ internal static string DroppingRequestDueToNoTargetObject { } } + /// + /// Looks up a localized string similar to This enumeration has already prefetched elements once.. + /// + internal static string ElementsAlreadyPrefetched { + get { + return ResourceManager.GetString("ElementsAlreadyPrefetched", resourceCulture); + } + } + /// /// Looks up a localized string similar to Error writing JSON RPC Message: {0}: {1}. /// @@ -581,14 +599,5 @@ internal static string UnsupportedPropertiesOnClientProxyInterface { return ResourceManager.GetString("UnsupportedPropertiesOnClientProxyInterface", resourceCulture); } } - - /// - /// Looks up a localized string similar to This method may only be called once and already has been.. - /// - internal static string UsableOnceOnly { - get { - return ResourceManager.GetString("UsableOnceOnly", resourceCulture); - } - } } } diff --git a/src/StreamJsonRpc/Resources.resx b/src/StreamJsonRpc/Resources.resx index a48a18a6..f3af3640 100644 --- a/src/StreamJsonRpc/Resources.resx +++ b/src/StreamJsonRpc/Resources.resx @@ -123,6 +123,9 @@ A CancellationToken is only allowed as the last parameter. + + This cannot be done after GetAsyncEnumerator has already been called. + "{0}" is not an interface. @@ -144,6 +147,9 @@ Got a request to execute '{0}' but have no callback object. Dropping the request. {0} is the method name to execute. + + This enumeration has already prefetched elements once. + Error writing JSON RPC Message: {0}: {1} {0} is the exception type, {1} is the exception message. @@ -313,7 +319,4 @@ Properties are not supported for service interfaces. - - This method may only be called once and already has been. - \ No newline at end of file diff --git a/src/StreamJsonRpc/netcoreapp2.1/PublicAPI.Unshipped.txt b/src/StreamJsonRpc/netcoreapp2.1/PublicAPI.Unshipped.txt index 0a887193..cb2e4bec 100644 --- a/src/StreamJsonRpc/netcoreapp2.1/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/netcoreapp2.1/PublicAPI.Unshipped.txt @@ -47,9 +47,7 @@ StreamJsonRpc.Reflection.JsonRpcResponseEventArgs.IsSuccessfulResponse.get -> bo StreamJsonRpc.Reflection.JsonRpcResponseEventArgs.JsonRpcResponseEventArgs(StreamJsonRpc.RequestId requestId, bool isSuccessfulResponse) -> void StreamJsonRpc.Reflection.MessageFormatterDuplexPipeTracker.MessageFormatterDuplexPipeTracker(StreamJsonRpc.JsonRpc jsonRpc, StreamJsonRpc.Reflection.IJsonRpcFormatterState formatterState) -> void StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker -StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker.CreateEnumerableProxy(System.Type enumeratedType, object handle) -> object -StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker.CreateEnumerableProxy(object handle) -> System.Collections.Generic.IAsyncEnumerable -StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker.GetToken(object enumerable) -> long +StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker.CreateEnumerableProxy(object handle, System.Collections.Generic.IReadOnlyList prefetchedItems) -> System.Collections.Generic.IAsyncEnumerable StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker.GetToken(System.Collections.Generic.IAsyncEnumerable enumerable) -> long StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker.MessageFormatterEnumerableTracker(StreamJsonRpc.JsonRpc jsonRpc, StreamJsonRpc.Reflection.IJsonRpcFormatterState formatterState) -> void StreamJsonRpc.Reflection.MessageFormatterProgressTracker.MessageFormatterProgressTracker(StreamJsonRpc.JsonRpc jsonRpc, StreamJsonRpc.Reflection.IJsonRpcFormatterState formatterState) -> void @@ -69,6 +67,8 @@ StreamJsonRpc.UnrecognizedJsonRpcMessageException.UnrecognizedJsonRpcMessageExce StreamJsonRpc.UnrecognizedJsonRpcMessageException.UnrecognizedJsonRpcMessageException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) -> void StreamJsonRpc.UnrecognizedJsonRpcMessageException.UnrecognizedJsonRpcMessageException(string message) -> void StreamJsonRpc.UnrecognizedJsonRpcMessageException.UnrecognizedJsonRpcMessageException(string message, System.Exception innerException) -> void +const StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker.TokenPropertyName = "token" -> string +const StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker.ValuesPropertyName = "values" -> string override StreamJsonRpc.PipeMessageHandler.DisposeReader() -> void override StreamJsonRpc.PipeMessageHandler.DisposeWriter() -> void override StreamJsonRpc.RemoteMethodNotFoundException.GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) -> void @@ -77,7 +77,10 @@ override StreamJsonRpc.RequestId.GetHashCode() -> int override StreamJsonRpc.RequestId.ToString() -> string static StreamJsonRpc.JsonRpcExtensions.AsAsyncEnumerable(this System.Collections.Generic.IEnumerable enumerable) -> System.Collections.Generic.IAsyncEnumerable static StreamJsonRpc.JsonRpcExtensions.AsAsyncEnumerable(this System.Collections.Generic.IEnumerable enumerable, StreamJsonRpc.JsonRpcEnumerableSettings settings) -> System.Collections.Generic.IAsyncEnumerable +static StreamJsonRpc.JsonRpcExtensions.AsAsyncEnumerable(this System.Collections.Generic.IEnumerable enumerable, StreamJsonRpc.JsonRpcEnumerableSettings settings, System.Threading.CancellationToken cancellationToken) -> System.Collections.Generic.IAsyncEnumerable +static StreamJsonRpc.JsonRpcExtensions.AsAsyncEnumerable(this System.Collections.Generic.IEnumerable enumerable, System.Threading.CancellationToken cancellationToken) -> System.Collections.Generic.IAsyncEnumerable static StreamJsonRpc.JsonRpcExtensions.WithJsonRpcSettings(this System.Collections.Generic.IAsyncEnumerable enumerable, StreamJsonRpc.JsonRpcEnumerableSettings settings) -> System.Collections.Generic.IAsyncEnumerable +static StreamJsonRpc.JsonRpcExtensions.WithPrefetchAsync(this System.Collections.Generic.IAsyncEnumerable enumerable, int count, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask> static StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker.CanDeserialize(System.Type objectType) -> bool static StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker.CanSerialize(System.Type objectType) -> bool static StreamJsonRpc.RequestId.NotSpecified.get -> StreamJsonRpc.RequestId diff --git a/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt b/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt index 0a887193..cb2e4bec 100644 --- a/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt @@ -47,9 +47,7 @@ StreamJsonRpc.Reflection.JsonRpcResponseEventArgs.IsSuccessfulResponse.get -> bo StreamJsonRpc.Reflection.JsonRpcResponseEventArgs.JsonRpcResponseEventArgs(StreamJsonRpc.RequestId requestId, bool isSuccessfulResponse) -> void StreamJsonRpc.Reflection.MessageFormatterDuplexPipeTracker.MessageFormatterDuplexPipeTracker(StreamJsonRpc.JsonRpc jsonRpc, StreamJsonRpc.Reflection.IJsonRpcFormatterState formatterState) -> void StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker -StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker.CreateEnumerableProxy(System.Type enumeratedType, object handle) -> object -StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker.CreateEnumerableProxy(object handle) -> System.Collections.Generic.IAsyncEnumerable -StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker.GetToken(object enumerable) -> long +StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker.CreateEnumerableProxy(object handle, System.Collections.Generic.IReadOnlyList prefetchedItems) -> System.Collections.Generic.IAsyncEnumerable StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker.GetToken(System.Collections.Generic.IAsyncEnumerable enumerable) -> long StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker.MessageFormatterEnumerableTracker(StreamJsonRpc.JsonRpc jsonRpc, StreamJsonRpc.Reflection.IJsonRpcFormatterState formatterState) -> void StreamJsonRpc.Reflection.MessageFormatterProgressTracker.MessageFormatterProgressTracker(StreamJsonRpc.JsonRpc jsonRpc, StreamJsonRpc.Reflection.IJsonRpcFormatterState formatterState) -> void @@ -69,6 +67,8 @@ StreamJsonRpc.UnrecognizedJsonRpcMessageException.UnrecognizedJsonRpcMessageExce StreamJsonRpc.UnrecognizedJsonRpcMessageException.UnrecognizedJsonRpcMessageException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) -> void StreamJsonRpc.UnrecognizedJsonRpcMessageException.UnrecognizedJsonRpcMessageException(string message) -> void StreamJsonRpc.UnrecognizedJsonRpcMessageException.UnrecognizedJsonRpcMessageException(string message, System.Exception innerException) -> void +const StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker.TokenPropertyName = "token" -> string +const StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker.ValuesPropertyName = "values" -> string override StreamJsonRpc.PipeMessageHandler.DisposeReader() -> void override StreamJsonRpc.PipeMessageHandler.DisposeWriter() -> void override StreamJsonRpc.RemoteMethodNotFoundException.GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) -> void @@ -77,7 +77,10 @@ override StreamJsonRpc.RequestId.GetHashCode() -> int override StreamJsonRpc.RequestId.ToString() -> string static StreamJsonRpc.JsonRpcExtensions.AsAsyncEnumerable(this System.Collections.Generic.IEnumerable enumerable) -> System.Collections.Generic.IAsyncEnumerable static StreamJsonRpc.JsonRpcExtensions.AsAsyncEnumerable(this System.Collections.Generic.IEnumerable enumerable, StreamJsonRpc.JsonRpcEnumerableSettings settings) -> System.Collections.Generic.IAsyncEnumerable +static StreamJsonRpc.JsonRpcExtensions.AsAsyncEnumerable(this System.Collections.Generic.IEnumerable enumerable, StreamJsonRpc.JsonRpcEnumerableSettings settings, System.Threading.CancellationToken cancellationToken) -> System.Collections.Generic.IAsyncEnumerable +static StreamJsonRpc.JsonRpcExtensions.AsAsyncEnumerable(this System.Collections.Generic.IEnumerable enumerable, System.Threading.CancellationToken cancellationToken) -> System.Collections.Generic.IAsyncEnumerable static StreamJsonRpc.JsonRpcExtensions.WithJsonRpcSettings(this System.Collections.Generic.IAsyncEnumerable enumerable, StreamJsonRpc.JsonRpcEnumerableSettings settings) -> System.Collections.Generic.IAsyncEnumerable +static StreamJsonRpc.JsonRpcExtensions.WithPrefetchAsync(this System.Collections.Generic.IAsyncEnumerable enumerable, int count, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask> static StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker.CanDeserialize(System.Type objectType) -> bool static StreamJsonRpc.Reflection.MessageFormatterEnumerableTracker.CanSerialize(System.Type objectType) -> bool static StreamJsonRpc.RequestId.NotSpecified.get -> StreamJsonRpc.RequestId diff --git a/src/StreamJsonRpc/xlf/Resources.cs.xlf b/src/StreamJsonRpc/xlf/Resources.cs.xlf index 6263b974..60626a7a 100644 --- a/src/StreamJsonRpc/xlf/Resources.cs.xlf +++ b/src/StreamJsonRpc/xlf/Resources.cs.xlf @@ -7,11 +7,21 @@ Parametr readable i writable jsou null. + + This cannot be done after GetAsyncEnumerator has already been called. + This cannot be done after GetAsyncEnumerator has already been called. + + Got a request to execute '{0}' but have no callback object. Dropping the request. Byla přijata žádost o provedení {0}, ale chybí objekt zpětného volání. Žádost se ruší. {0} is the method name to execute. + + This enumeration has already prefetched elements once. + This enumeration has already prefetched elements once. + + Error writing JSON RPC Result: {0}: {1} Chyba při zápisu výsledku JSON RPC: {0}: {1} @@ -292,11 +302,6 @@ Chyba při zápisu zprávy JSON RPC: {0}: {1} {0} is the exception type, {1} is the exception message. - - This method may only be called once and already has been. - This method may only be called once and already has been. - - \ No newline at end of file diff --git a/src/StreamJsonRpc/xlf/Resources.de.xlf b/src/StreamJsonRpc/xlf/Resources.de.xlf index 302e53a4..a2de9b3c 100644 --- a/src/StreamJsonRpc/xlf/Resources.de.xlf +++ b/src/StreamJsonRpc/xlf/Resources.de.xlf @@ -7,11 +7,21 @@ "readable" und "writable" sind NULL. + + This cannot be done after GetAsyncEnumerator has already been called. + This cannot be done after GetAsyncEnumerator has already been called. + + Got a request to execute '{0}' but have no callback object. Dropping the request. Eine Anforderung zum Ausführen von "{0}" wurde ausgegeben, es ist aber kein Rückrufobjekt vorhanden. Die Anforderung wird gelöscht. {0} is the method name to execute. + + This enumeration has already prefetched elements once. + This enumeration has already prefetched elements once. + + Error writing JSON RPC Result: {0}: {1} Fehler beim Schreiben des JSON-RPC-Ergebnisses: {0}: {1} @@ -292,11 +302,6 @@ Fehler beim Schreiben der JSON-RPC-Nachricht: {0}: {1} {0} is the exception type, {1} is the exception message. - - This method may only be called once and already has been. - This method may only be called once and already has been. - - \ No newline at end of file diff --git a/src/StreamJsonRpc/xlf/Resources.es.xlf b/src/StreamJsonRpc/xlf/Resources.es.xlf index e92e47b7..48dc9a96 100644 --- a/src/StreamJsonRpc/xlf/Resources.es.xlf +++ b/src/StreamJsonRpc/xlf/Resources.es.xlf @@ -7,11 +7,21 @@ La lectura y escritura son NULL. + + This cannot be done after GetAsyncEnumerator has already been called. + This cannot be done after GetAsyncEnumerator has already been called. + + Got a request to execute '{0}' but have no callback object. Dropping the request. Recibió una solicitud para ejecutar '{0}', pero no hay ningún objeto de devolución de llamada. Anulando la solicitud. {0} is the method name to execute. + + This enumeration has already prefetched elements once. + This enumeration has already prefetched elements once. + + Error writing JSON RPC Result: {0}: {1} Error al escribir el resultado de JSON RPC: {0}: {1} @@ -292,11 +302,6 @@ Error al escribir el mensaje de JSON RPC: {0}: {1} {0} is the exception type, {1} is the exception message. - - This method may only be called once and already has been. - This method may only be called once and already has been. - - \ No newline at end of file diff --git a/src/StreamJsonRpc/xlf/Resources.fr.xlf b/src/StreamJsonRpc/xlf/Resources.fr.xlf index ea9cdd93..d53971a7 100644 --- a/src/StreamJsonRpc/xlf/Resources.fr.xlf +++ b/src/StreamJsonRpc/xlf/Resources.fr.xlf @@ -7,11 +7,21 @@ readable et writable sont tous les deux Null. + + This cannot be done after GetAsyncEnumerator has already been called. + This cannot be done after GetAsyncEnumerator has already been called. + + Got a request to execute '{0}' but have no callback object. Dropping the request. A reçu une demande pour exécuter '{0}' mais n’a aucun objet de rappel. Suppression de la demande. {0} is the method name to execute. + + This enumeration has already prefetched elements once. + This enumeration has already prefetched elements once. + + Error writing JSON RPC Result: {0}: {1} Erreur lors de l’écriture du résultat de JSON RPC : {0} : {1} @@ -292,11 +302,6 @@ Erreur lors de l'écriture du message RPC JSON : {0} : {1} {0} is the exception type, {1} is the exception message. - - This method may only be called once and already has been. - This method may only be called once and already has been. - - \ No newline at end of file diff --git a/src/StreamJsonRpc/xlf/Resources.it.xlf b/src/StreamJsonRpc/xlf/Resources.it.xlf index 00d73c95..4e5b5584 100644 --- a/src/StreamJsonRpc/xlf/Resources.it.xlf +++ b/src/StreamJsonRpc/xlf/Resources.it.xlf @@ -7,11 +7,21 @@ Readable e Writable sono entrambi Null. + + This cannot be done after GetAsyncEnumerator has already been called. + This cannot be done after GetAsyncEnumerator has already been called. + + Got a request to execute '{0}' but have no callback object. Dropping the request. È stata ricevuta una richiesta per l'esecuzione di '{0}' ma non sono presenti oggetti di callback. La richiesta verrà eliminata. {0} is the method name to execute. + + This enumeration has already prefetched elements once. + This enumeration has already prefetched elements once. + + Error writing JSON RPC Result: {0}: {1} Si è verificato un errore durante la scrittura del risultato della RPC JSON. {0}: {1} @@ -292,11 +302,6 @@ Si è verificato un errore durante la scrittura del messaggio della RPC JSON. {0}: {1} {0} is the exception type, {1} is the exception message. - - This method may only be called once and already has been. - This method may only be called once and already has been. - - \ No newline at end of file diff --git a/src/StreamJsonRpc/xlf/Resources.ja.xlf b/src/StreamJsonRpc/xlf/Resources.ja.xlf index 446cbfd3..aee840c1 100644 --- a/src/StreamJsonRpc/xlf/Resources.ja.xlf +++ b/src/StreamJsonRpc/xlf/Resources.ja.xlf @@ -7,11 +7,21 @@ Readable と Writable の両方が null です。 + + This cannot be done after GetAsyncEnumerator has already been called. + This cannot be done after GetAsyncEnumerator has already been called. + + Got a request to execute '{0}' but have no callback object. Dropping the request. '{0}' の実行の要求を受け取りましたが、コールバック オブジェクトがありません。要求を削除しています。 {0} is the method name to execute. + + This enumeration has already prefetched elements once. + This enumeration has already prefetched elements once. + + Error writing JSON RPC Result: {0}: {1} JSON RPC 結果の書き込み時のエラー: {0}: {1} @@ -292,11 +302,6 @@ JSON RPC メッセージの書き込み時のエラー: {0}: {1} {0} is the exception type, {1} is the exception message. - - This method may only be called once and already has been. - This method may only be called once and already has been. - - \ No newline at end of file diff --git a/src/StreamJsonRpc/xlf/Resources.ko.xlf b/src/StreamJsonRpc/xlf/Resources.ko.xlf index 0ecdccae..632a777b 100644 --- a/src/StreamJsonRpc/xlf/Resources.ko.xlf +++ b/src/StreamJsonRpc/xlf/Resources.ko.xlf @@ -7,11 +7,21 @@ 읽고 쓸 수 있는 개체는 null입니다. + + This cannot be done after GetAsyncEnumerator has already been called. + This cannot be done after GetAsyncEnumerator has already been called. + + Got a request to execute '{0}' but have no callback object. Dropping the request. ‘{0}’을(를) 실행하도록 요청을 받았지만 이 요청에 콜백 개체가 없습니다. 요청을 삭제합니다. {0} is the method name to execute. + + This enumeration has already prefetched elements once. + This enumeration has already prefetched elements once. + + Error writing JSON RPC Result: {0}: {1} JSON RPC 결과를 쓰는 동안 오류 발생: {0}:{1} @@ -292,11 +302,6 @@ JSON RPC 메시지를 쓰는 동안 오류 발생: {0}: {1} {0} is the exception type, {1} is the exception message. - - This method may only be called once and already has been. - This method may only be called once and already has been. - - \ No newline at end of file diff --git a/src/StreamJsonRpc/xlf/Resources.pl.xlf b/src/StreamJsonRpc/xlf/Resources.pl.xlf index 171b2cb6..b5b255e6 100644 --- a/src/StreamJsonRpc/xlf/Resources.pl.xlf +++ b/src/StreamJsonRpc/xlf/Resources.pl.xlf @@ -7,11 +7,21 @@ Elementy readable i writable mają wartość null. + + This cannot be done after GetAsyncEnumerator has already been called. + This cannot be done after GetAsyncEnumerator has already been called. + + Got a request to execute '{0}' but have no callback object. Dropping the request. Otrzymano żądanie wykonania metody „{0}”, ale brakuje obiektu wywołania zwrotnego. Żądanie zostanie odrzucone. {0} is the method name to execute. + + This enumeration has already prefetched elements once. + This enumeration has already prefetched elements once. + + Error writing JSON RPC Result: {0}: {1} Błąd zapisywania wyniku JSON RPC: {0}: {1} @@ -292,11 +302,6 @@ Błąd zapisywania komunikatu JSON zdalnego wywołania procedury: {0}: {1} {0} is the exception type, {1} is the exception message. - - This method may only be called once and already has been. - This method may only be called once and already has been. - - \ No newline at end of file diff --git a/src/StreamJsonRpc/xlf/Resources.pt-BR.xlf b/src/StreamJsonRpc/xlf/Resources.pt-BR.xlf index 11d280d2..2a6426d4 100644 --- a/src/StreamJsonRpc/xlf/Resources.pt-BR.xlf +++ b/src/StreamJsonRpc/xlf/Resources.pt-BR.xlf @@ -7,11 +7,21 @@ Legível e gravável são nulos. + + This cannot be done after GetAsyncEnumerator has already been called. + This cannot be done after GetAsyncEnumerator has already been called. + + Got a request to execute '{0}' but have no callback object. Dropping the request. Recebeu uma solicitação para executar '{0}', mas não há nenhum objeto de retorno de chamada. Removendo a solicitação. {0} is the method name to execute. + + This enumeration has already prefetched elements once. + This enumeration has already prefetched elements once. + + Error writing JSON RPC Result: {0}: {1} Erro ao gravar o resultado de RPC de JSON: {0}: {1} @@ -292,11 +302,6 @@ Erro ao gravar a Mensagem da RPC do JSON: {0}: {1} {0} is the exception type, {1} is the exception message. - - This method may only be called once and already has been. - This method may only be called once and already has been. - - \ No newline at end of file diff --git a/src/StreamJsonRpc/xlf/Resources.ru.xlf b/src/StreamJsonRpc/xlf/Resources.ru.xlf index ca32b955..5758ea71 100644 --- a/src/StreamJsonRpc/xlf/Resources.ru.xlf +++ b/src/StreamJsonRpc/xlf/Resources.ru.xlf @@ -7,11 +7,21 @@ И элементы, которые можно считать, и элементы, которые доступны для записи, имеют значение NULL. + + This cannot be done after GetAsyncEnumerator has already been called. + This cannot be done after GetAsyncEnumerator has already been called. + + Got a request to execute '{0}' but have no callback object. Dropping the request. Получен запрос на выполнение "{0}", но отсутствует объект обратного вызова. Удаление запроса. {0} is the method name to execute. + + This enumeration has already prefetched elements once. + This enumeration has already prefetched elements once. + + Error writing JSON RPC Result: {0}: {1} Произошла ошибка при записи результата RPC JSON. {0}: {1} @@ -292,11 +302,6 @@ Произошла ошибка при записи сообщения RPC JSON. {0}: {1} {0} is the exception type, {1} is the exception message. - - This method may only be called once and already has been. - This method may only be called once and already has been. - - \ No newline at end of file diff --git a/src/StreamJsonRpc/xlf/Resources.tr.xlf b/src/StreamJsonRpc/xlf/Resources.tr.xlf index 67e85653..2eb532f8 100644 --- a/src/StreamJsonRpc/xlf/Resources.tr.xlf +++ b/src/StreamJsonRpc/xlf/Resources.tr.xlf @@ -7,11 +7,21 @@ Readable ve writable null. + + This cannot be done after GetAsyncEnumerator has already been called. + This cannot be done after GetAsyncEnumerator has already been called. + + Got a request to execute '{0}' but have no callback object. Dropping the request. '{0}' metodunu yürütme isteği alındı ancak geri çağırma nesnesi yok. İstek bırakılıyor. {0} is the method name to execute. + + This enumeration has already prefetched elements once. + This enumeration has already prefetched elements once. + + Error writing JSON RPC Result: {0}: {1} JSON RPC Sonucu yazılırken bir hata oluştu: {0}: {1} @@ -292,11 +302,6 @@ JSON RPC İletisi yazılırken bir hata oluştu: {0}: {1} {0} is the exception type, {1} is the exception message. - - This method may only be called once and already has been. - This method may only be called once and already has been. - - \ No newline at end of file diff --git a/src/StreamJsonRpc/xlf/Resources.zh-Hans.xlf b/src/StreamJsonRpc/xlf/Resources.zh-Hans.xlf index bbe84eb7..fe8913f1 100644 --- a/src/StreamJsonRpc/xlf/Resources.zh-Hans.xlf +++ b/src/StreamJsonRpc/xlf/Resources.zh-Hans.xlf @@ -7,11 +7,21 @@ 可读内容和可写内容均为 null。 + + This cannot be done after GetAsyncEnumerator has already been called. + This cannot be done after GetAsyncEnumerator has already been called. + + Got a request to execute '{0}' but have no callback object. Dropping the request. 已获得执行“{0}”的请求,但没有回调对象。删除请求。 {0} is the method name to execute. + + This enumeration has already prefetched elements once. + This enumeration has already prefetched elements once. + + Error writing JSON RPC Result: {0}: {1} 写入 JSON RPC 结果 {0} 时出错:{1} @@ -292,11 +302,6 @@ 写入 JSON RPC 消息时出错: {0}: {1} {0} is the exception type, {1} is the exception message. - - This method may only be called once and already has been. - This method may only be called once and already has been. - - \ No newline at end of file diff --git a/src/StreamJsonRpc/xlf/Resources.zh-Hant.xlf b/src/StreamJsonRpc/xlf/Resources.zh-Hant.xlf index 356a5eb4..599c3d9d 100644 --- a/src/StreamJsonRpc/xlf/Resources.zh-Hant.xlf +++ b/src/StreamJsonRpc/xlf/Resources.zh-Hant.xlf @@ -7,11 +7,21 @@ Readable 和 Writable 都是 Null。 + + This cannot be done after GetAsyncEnumerator has already been called. + This cannot be done after GetAsyncEnumerator has already been called. + + Got a request to execute '{0}' but have no callback object. Dropping the request. 取得執行 '{0}' 的要求,但沒有回呼物件。正在卸除要求。 {0} is the method name to execute. + + This enumeration has already prefetched elements once. + This enumeration has already prefetched elements once. + + Error writing JSON RPC Result: {0}: {1} 寫入 JSON RPC 結果時發生錯誤: {0}: {1} @@ -292,11 +302,6 @@ 寫入 JSON RPC 訊息時發生錯誤: {0}: {1} {0} is the exception type, {1} is the exception message. - - This method may only be called once and already has been. - This method may only be called once and already has been. - - \ No newline at end of file