/
JSRuntime.cs
254 lines (223 loc) · 12.3 KB
/
JSRuntime.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.JSInterop.Implementation;
using Microsoft.JSInterop.Infrastructure;
namespace Microsoft.JSInterop
{
/// <summary>
/// Abstract base class for a JavaScript runtime.
/// </summary>
public abstract partial class JSRuntime : IJSRuntime
{
private long _nextObjectReferenceId = 0; // 0 signals no object, but we increment prior to assignment. The first tracked object should have id 1
private long _nextPendingTaskId = 1; // Start at 1 because zero signals "no response needed"
private readonly ConcurrentDictionary<long, object> _pendingTasks = new ConcurrentDictionary<long, object>();
private readonly ConcurrentDictionary<long, IDotNetObjectReference> _trackedRefsById = new ConcurrentDictionary<long, IDotNetObjectReference>();
private readonly ConcurrentDictionary<long, CancellationTokenRegistration> _cancellationRegistrations =
new ConcurrentDictionary<long, CancellationTokenRegistration>();
/// <summary>
/// Initializes a new instance of <see cref="JSRuntime"/>.
/// </summary>
protected JSRuntime()
{
JsonSerializerOptions = new JsonSerializerOptions
{
MaxDepth = 32,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
Converters =
{
new DotNetObjectReferenceJsonConverterFactory(this),
new JSObjectReferenceJsonConverter<IJSObjectReference, JSObjectReference>(
id => new JSObjectReference(this, id)),
}
};
}
/// <summary>
/// Gets the <see cref="System.Text.Json.JsonSerializerOptions"/> used to serialize and deserialize interop payloads.
/// </summary>
protected internal JsonSerializerOptions JsonSerializerOptions { get; }
/// <summary>
/// Gets or sets the default timeout for asynchronous JavaScript calls.
/// </summary>
protected TimeSpan? DefaultAsyncTimeout { get; set; }
/// <summary>
/// Invokes the specified JavaScript function asynchronously.
/// <para>
/// <see cref="JSRuntime"/> will apply timeouts to this operation based on the value configured in <see cref="DefaultAsyncTimeout"/>. To dispatch a call with a different, or no timeout,
/// consider using <see cref="InvokeAsync{TValue}(string, CancellationToken, object[])" />.
/// </para>
/// </summary>
/// <typeparam name="TValue">The JSON-serializable return type.</typeparam>
/// <param name="identifier">An identifier for the function to invoke. For example, the value <c>"someScope.someFunction"</c> will invoke the function <c>window.someScope.someFunction</c>.</param>
/// <param name="args">JSON-serializable arguments.</param>
/// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns>
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, object?[]? args)
=> InvokeAsync<TValue>(0, identifier, args);
/// <summary>
/// Invokes the specified JavaScript function asynchronously.
/// </summary>
/// <typeparam name="TValue">The JSON-serializable return type.</typeparam>
/// <param name="identifier">An identifier for the function to invoke. For example, the value <c>"someScope.someFunction"</c> will invoke the function <c>window.someScope.someFunction</c>.</param>
/// <param name="cancellationToken">
/// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts
/// (<see cref="DefaultAsyncTimeout"/>) from being applied.
/// </param>
/// <param name="args">JSON-serializable arguments.</param>
/// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns>
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken, object?[]? args)
=> InvokeAsync<TValue>(0, identifier, cancellationToken, args);
internal async ValueTask<TValue> InvokeAsync<TValue>(long targetInstanceId, string identifier, object?[]? args)
{
if (DefaultAsyncTimeout.HasValue)
{
using var cts = new CancellationTokenSource(DefaultAsyncTimeout.Value);
// We need to await here due to the using
return await InvokeAsync<TValue>(targetInstanceId, identifier, cts.Token, args);
}
return await InvokeAsync<TValue>(targetInstanceId, identifier, CancellationToken.None, args);
}
internal ValueTask<TValue> InvokeAsync<TValue>(
long targetInstanceId,
string identifier,
CancellationToken cancellationToken,
object?[]? args)
{
var taskId = Interlocked.Increment(ref _nextPendingTaskId);
var tcs = new TaskCompletionSource<TValue>();
if (cancellationToken.CanBeCanceled)
{
_cancellationRegistrations[taskId] = cancellationToken.Register(() =>
{
tcs.TrySetCanceled(cancellationToken);
CleanupTasksAndRegistrations(taskId);
});
}
_pendingTasks[taskId] = tcs;
try
{
if (cancellationToken.IsCancellationRequested)
{
tcs.TrySetCanceled(cancellationToken);
CleanupTasksAndRegistrations(taskId);
return new ValueTask<TValue>(tcs.Task);
}
var argsJson = args?.Any() == true ?
JsonSerializer.Serialize(args, JsonSerializerOptions) :
null;
var resultType = JSCallResultTypeHelper.FromGeneric<TValue>();
BeginInvokeJS(taskId, identifier, argsJson, resultType, targetInstanceId);
return new ValueTask<TValue>(tcs.Task);
}
catch
{
CleanupTasksAndRegistrations(taskId);
throw;
}
}
private void CleanupTasksAndRegistrations(long taskId)
{
_pendingTasks.TryRemove(taskId, out _);
if (_cancellationRegistrations.TryRemove(taskId, out var registration))
{
registration.Dispose();
}
}
/// <summary>
/// Begins an asynchronous function invocation.
/// </summary>
/// <param name="taskId">The identifier for the function invocation, or zero if no async callback is required.</param>
/// <param name="identifier">The identifier for the function to invoke.</param>
/// <param name="argsJson">A JSON representation of the arguments.</param>
protected virtual void BeginInvokeJS(long taskId, string identifier, string? argsJson)
=> BeginInvokeJS(taskId, identifier, argsJson, JSCallResultType.Default, 0);
/// <summary>
/// Begins an asynchronous function invocation.
/// </summary>
/// <param name="taskId">The identifier for the function invocation, or zero if no async callback is required.</param>
/// <param name="identifier">The identifier for the function to invoke.</param>
/// <param name="argsJson">A JSON representation of the arguments.</param>
/// <param name="resultType">The type of result expected from the invocation.</param>
/// <param name="targetInstanceId">The instance ID of the target JS object.</param>
protected abstract void BeginInvokeJS(long taskId, string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId);
/// <summary>
/// Completes an async JS interop call from JavaScript to .NET
/// </summary>
/// <param name="invocationInfo">The <see cref="DotNetInvocationInfo"/>.</param>
/// <param name="invocationResult">The <see cref="DotNetInvocationResult"/>.</param>
protected internal abstract void EndInvokeDotNet(
DotNetInvocationInfo invocationInfo,
in DotNetInvocationResult invocationResult);
internal void EndInvokeJS(long taskId, bool succeeded, ref Utf8JsonReader jsonReader)
{
if (!_pendingTasks.TryRemove(taskId, out var tcs))
{
// We should simply return if we can't find an id for the invocation.
// This likely means that the method that initiated the call defined a timeout and stopped waiting.
return;
}
CleanupTasksAndRegistrations(taskId);
try
{
if (succeeded)
{
var resultType = TaskGenericsUtil.GetTaskCompletionSourceResultType(tcs);
var result = JsonSerializer.Deserialize(ref jsonReader, resultType, JsonSerializerOptions);
TaskGenericsUtil.SetTaskCompletionSourceResult(tcs, result);
}
else
{
var exceptionText = jsonReader.GetString() ?? string.Empty;
TaskGenericsUtil.SetTaskCompletionSourceException(tcs, new JSException(exceptionText));
}
}
catch (Exception exception)
{
var message = $"An exception occurred executing JS interop: {exception.Message}. See InnerException for more details.";
TaskGenericsUtil.SetTaskCompletionSourceException(tcs, new JSException(message, exception));
}
}
internal long TrackObjectReference<TValue>(DotNetObjectReference<TValue> dotNetObjectReference) where TValue : class
{
if (dotNetObjectReference == null)
{
throw new ArgumentNullException(nameof(dotNetObjectReference));
}
dotNetObjectReference.ThrowIfDisposed();
var jsRuntime = dotNetObjectReference.JSRuntime;
if (jsRuntime is null)
{
var dotNetObjectId = Interlocked.Increment(ref _nextObjectReferenceId);
dotNetObjectReference.JSRuntime = this;
dotNetObjectReference.ObjectId = dotNetObjectId;
_trackedRefsById[dotNetObjectId] = dotNetObjectReference;
}
else if (!ReferenceEquals(this, jsRuntime))
{
throw new InvalidOperationException($"{dotNetObjectReference.GetType().Name} is already being tracked by a different instance of {nameof(JSRuntime)}." +
$" A common cause is caching an instance of {nameof(DotNetObjectReference<TValue>)} globally. Consider creating instances of {nameof(DotNetObjectReference<TValue>)} at the JSInterop callsite.");
}
Debug.Assert(dotNetObjectReference.ObjectId != 0);
return dotNetObjectReference.ObjectId;
}
internal IDotNetObjectReference GetObjectReference(long dotNetObjectId)
{
return _trackedRefsById.TryGetValue(dotNetObjectId, out var dotNetObjectRef)
? dotNetObjectRef
: throw new ArgumentException($"There is no tracked object with id '{dotNetObjectId}'. Perhaps the DotNetObjectReference instance was already disposed.", nameof(dotNetObjectId));
}
/// <summary>
/// Stops tracking the specified .NET object reference.
/// This may be invoked either by disposing a DotNetObjectRef in .NET code, or via JS interop by calling "dispose" on the corresponding instance in JavaScript code
/// </summary>
/// <param name="dotNetObjectId">The ID of the <see cref="DotNetObjectReference{TValue}"/>.</param>
internal void ReleaseObjectReference(long dotNetObjectId) => _trackedRefsById.TryRemove(dotNetObjectId, out _);
}
}