Eliminate ElementRef's static incrementing ID for remote rendering cases
... because it's important not to disclose cross-user state, such as the number of IDs that have been assigned. Plus we don't want to run out of unique IDs, which we could if it's limited by the range of an 'int'.
This commit is contained in:
parent
c2f3128ef9
commit
767a2373c8
|
|
@ -153,7 +153,7 @@ export class BrowserRenderer {
|
|||
return this.insertFrameRange(batch, componentId, parent, childIndex, frames, frameIndex + 1, frameIndex + frameReader.subtreeLength(frame));
|
||||
case FrameType.elementReferenceCapture:
|
||||
if (parent instanceof Element) {
|
||||
applyCaptureIdToElement(parent, frameReader.elementReferenceCaptureId(frame));
|
||||
applyCaptureIdToElement(parent, frameReader.elementReferenceCaptureId(frame)!);
|
||||
return 0; // A "capture" is a child in the diff, but has no node in the DOM
|
||||
} else {
|
||||
throw new Error('Reference capture frames can only be children of element frames.');
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
export function applyCaptureIdToElement(element: Element, referenceCaptureId: number) {
|
||||
export function applyCaptureIdToElement(element: Element, referenceCaptureId: string) {
|
||||
element.setAttribute(getCaptureIdAttributeName(referenceCaptureId), '');
|
||||
}
|
||||
|
||||
function getElementByCaptureId(referenceCaptureId: number) {
|
||||
function getElementByCaptureId(referenceCaptureId: string) {
|
||||
const selector = `[${getCaptureIdAttributeName(referenceCaptureId)}]`;
|
||||
return document.querySelector(selector);
|
||||
}
|
||||
|
||||
function getCaptureIdAttributeName(referenceCaptureId: number) {
|
||||
function getCaptureIdAttributeName(referenceCaptureId: string) {
|
||||
return `_bl_${referenceCaptureId}`;
|
||||
}
|
||||
|
||||
// Support receiving ElementRef instances as args in interop calls
|
||||
const elementRefKey = '_blazorElementRef'; // Keep in sync with ElementRef.cs
|
||||
DotNet.attachReviver((key, value) => {
|
||||
if (value && typeof value === 'object' && value.hasOwnProperty(elementRefKey) && typeof value[elementRefKey] === 'number') {
|
||||
if (value && typeof value === 'object' && value.hasOwnProperty(elementRefKey) && typeof value[elementRefKey] === 'string') {
|
||||
return getElementByCaptureId(value[elementRefKey]);
|
||||
} else {
|
||||
return value;
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export interface RenderTreeEditReader {
|
|||
export interface RenderTreeFrameReader {
|
||||
frameType(frame: RenderTreeFrame): FrameType;
|
||||
subtreeLength(frame: RenderTreeFrame): number;
|
||||
elementReferenceCaptureId(frame: RenderTreeFrame): number;
|
||||
elementReferenceCaptureId(frame: RenderTreeFrame): string | null;
|
||||
componentId(frame: RenderTreeFrame): number;
|
||||
elementName(frame: RenderTreeFrame): string | null;
|
||||
textContent(frame: RenderTreeFrame): string | null;
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ const frameReader = {
|
|||
structLength: 28,
|
||||
frameType: (frame: RenderTreeFrame) => platform.readInt32Field(frame as any, 4) as FrameType,
|
||||
subtreeLength: (frame: RenderTreeFrame) => platform.readInt32Field(frame as any, 8),
|
||||
elementReferenceCaptureId: (frame: RenderTreeFrame) => platform.readInt32Field(frame as any, 8),
|
||||
elementReferenceCaptureId: (frame: RenderTreeFrame) => platform.readStringField(frame as any, 16),
|
||||
componentId: (frame: RenderTreeFrame) => platform.readInt32Field(frame as any, 12),
|
||||
elementName: (frame: RenderTreeFrame) => platform.readStringField(frame as any, 16),
|
||||
textContent: (frame: RenderTreeFrame) => platform.readStringField(frame as any, 16),
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ namespace Microsoft.AspNetCore.Blazor.Server.Circuits
|
|||
WritePadding(_binaryWriter, 4);
|
||||
break;
|
||||
case RenderTreeFrameType.ElementReferenceCapture:
|
||||
_binaryWriter.Write(frame.ElementReferenceCaptureId);
|
||||
WriteString(frame.ElementReferenceCaptureId);
|
||||
WritePadding(_binaryWriter, 8);
|
||||
break;
|
||||
case RenderTreeFrameType.Region:
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.JSInterop.Internal;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
|
|
@ -12,25 +13,18 @@ namespace Microsoft.AspNetCore.Blazor
|
|||
/// </summary>
|
||||
public readonly struct ElementRef : ICustomJsonSerializer
|
||||
{
|
||||
// Static to ensure uniqueness even if there are multiple Renderer instances
|
||||
// This would not be necessary if the JS-side code maintained a lookup from capureId to Element instances,
|
||||
// but we're not doing that presently as it causes more work during disposal to remove those entries
|
||||
// WARNING: Once we support server-side rendering, we should check if running on the server and avoid
|
||||
// populating element reference capture IDs at all, because doing so could (a) eventually
|
||||
// overflow the static int, and (b) disclose information to clients about how many other
|
||||
// requests the server is handling, etc. In general, as part of implementing SSR, we need to
|
||||
// audit the code it calls for any use of statics.
|
||||
private static int _nextId = 0;
|
||||
static long _nextIdForWebAssemblyOnly = 1;
|
||||
|
||||
internal int Id { get; }
|
||||
// The Id is unique at least within the scope of a given user/circuit
|
||||
internal string Id { get; }
|
||||
|
||||
private ElementRef(int id)
|
||||
private ElementRef(string id)
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
|
||||
internal static ElementRef CreateWithUniqueId()
|
||||
=> new ElementRef(Interlocked.Increment(ref _nextId));
|
||||
=> new ElementRef(CreateUniqueId());
|
||||
|
||||
object ICustomJsonSerializer.ToJsonPrimitive()
|
||||
{
|
||||
|
|
@ -39,5 +33,26 @@ namespace Microsoft.AspNetCore.Blazor
|
|||
{ "_blazorElementRef", Id }
|
||||
};
|
||||
}
|
||||
|
||||
static string CreateUniqueId()
|
||||
{
|
||||
if (PlatformInfo.IsWebAssembly)
|
||||
{
|
||||
// On WebAssembly there's only one user, so it's fine to expose the number
|
||||
// of IDs that have been assigned, and this is cheaper than creating a GUID.
|
||||
// It's unfortunate that this still involves a heap allocation. If that becomes
|
||||
// a problem we could extend RenderTreeFrame to have both "string" and "long"
|
||||
// fields for ElementRefCaptureId, of which only one would be in use depending
|
||||
// on the platform.
|
||||
var id = Interlocked.Increment(ref _nextIdForWebAssemblyOnly);
|
||||
return id.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
// For remote rendering, it's important not to disclose any cross-user state,
|
||||
// such as the number of IDs that have been assigned.
|
||||
return Guid.NewGuid().ToString("D");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
// 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.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor
|
||||
{
|
||||
internal static class PlatformInfo
|
||||
{
|
||||
public static bool IsWebAssembly { get; }
|
||||
|
||||
static PlatformInfo()
|
||||
{
|
||||
IsWebAssembly = RuntimeInformation.IsOSPlatform(OSPlatform.Create("WEBASSEMBLY"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -137,13 +137,13 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
|
|||
/// If the <see cref="FrameType"/> property equals <see cref="RenderTreeFrameType.ElementReferenceCapture"/>,
|
||||
/// gets the ID of the reference capture. Otherwise, the value is undefined.
|
||||
/// </summary>
|
||||
[FieldOffset(8)] public readonly int ElementReferenceCaptureId;
|
||||
[FieldOffset(16)] public readonly string ElementReferenceCaptureId;
|
||||
|
||||
/// <summary>
|
||||
/// If the <see cref="FrameType"/> property equals <see cref="RenderTreeFrameType.ElementReferenceCapture"/>,
|
||||
/// gets the action that writes the reference to its target. Otherwise, the value is undefined.
|
||||
/// </summary>
|
||||
[FieldOffset(16)] public readonly Action<ElementRef> ElementReferenceCaptureAction;
|
||||
[FieldOffset(24)] public readonly Action<ElementRef> ElementReferenceCaptureAction;
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
// RenderTreeFrameType.ComponentReferenceCapture
|
||||
|
|
@ -227,7 +227,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
|
|||
RegionSubtreeLength = regionSubtreeLength;
|
||||
}
|
||||
|
||||
private RenderTreeFrame(int sequence, Action<ElementRef> elementReferenceCaptureAction, int elementReferenceCaptureId)
|
||||
private RenderTreeFrame(int sequence, Action<ElementRef> elementReferenceCaptureAction, string elementReferenceCaptureId)
|
||||
: this()
|
||||
{
|
||||
FrameType = RenderTreeFrameType.ElementReferenceCapture;
|
||||
|
|
@ -264,7 +264,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
|
|||
=> new RenderTreeFrame(sequence, regionSubtreeLength: 0);
|
||||
|
||||
internal static RenderTreeFrame ElementReferenceCapture(int sequence, Action<ElementRef> elementReferenceCaptureAction)
|
||||
=> new RenderTreeFrame(sequence, elementReferenceCaptureAction: elementReferenceCaptureAction, elementReferenceCaptureId: 0);
|
||||
=> new RenderTreeFrame(sequence, elementReferenceCaptureAction: elementReferenceCaptureAction, elementReferenceCaptureId: null);
|
||||
|
||||
internal static RenderTreeFrame ComponentReferenceCapture(int sequence, Action<object> componentReferenceCaptureAction, int parentFrameIndex)
|
||||
=> new RenderTreeFrame(sequence, componentReferenceCaptureAction: componentReferenceCaptureAction, parentFrameIndex: parentFrameIndex);
|
||||
|
|
@ -287,7 +287,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
|
|||
internal RenderTreeFrame WithRegionSubtreeLength(int regionSubtreeLength)
|
||||
=> new RenderTreeFrame(Sequence, regionSubtreeLength: regionSubtreeLength);
|
||||
|
||||
internal RenderTreeFrame WithElementReferenceCaptureId(int elementReferenceCaptureId)
|
||||
internal RenderTreeFrame WithElementReferenceCaptureId(string elementReferenceCaptureId)
|
||||
=> new RenderTreeFrame(Sequence, ElementReferenceCaptureAction, elementReferenceCaptureId);
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// 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 Microsoft.AspNetCore.Blazor.Components;
|
||||
|
|
@ -1342,9 +1342,9 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
|||
// Act
|
||||
var (diff, referenceFrames) = GetSingleUpdatedComponent();
|
||||
|
||||
// Assert: Distinct nonzero IDs
|
||||
Assert.NotEqual(0, ref1.Id);
|
||||
Assert.NotEqual(0, ref2.Id);
|
||||
// Assert: Distinct nonnull IDs
|
||||
Assert.NotNull(ref1.Id);
|
||||
Assert.NotNull(ref2.Id);
|
||||
Assert.NotEqual(ref1.Id, ref2.Id);
|
||||
|
||||
// Assert: Also specified in diff
|
||||
|
|
@ -1388,7 +1388,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
|||
// Note: We're not preserving the ReferenceCaptureId on the actual RenderTreeFrames in the same
|
||||
// way we do for event handler IDs, simply because there's no need to do so. We only do
|
||||
// anything with ReferenceCaptureId when frames are first inserted into the document.
|
||||
Assert.NotEqual(0, ref1.Id);
|
||||
Assert.NotNull(ref1.Id);
|
||||
Assert.Equal(1, refWriteCount);
|
||||
Assert.Empty(diff.Edits);
|
||||
Assert.Empty(referenceFrames);
|
||||
|
|
|
|||
|
|
@ -194,7 +194,7 @@ namespace Microsoft.AspNetCore.Blazor.Server
|
|||
RenderTreeFrame.Element(128, "Some element")
|
||||
.WithElementSubtreeLength(1234),
|
||||
RenderTreeFrame.ElementReferenceCapture(129, value => { })
|
||||
.WithElementReferenceCaptureId(12121),
|
||||
.WithElementReferenceCaptureId("my unique ID"),
|
||||
RenderTreeFrame.Region(130)
|
||||
.WithRegionSubtreeLength(1234),
|
||||
RenderTreeFrame.Text(131, "Some text"),
|
||||
|
|
@ -212,7 +212,7 @@ namespace Microsoft.AspNetCore.Blazor.Server
|
|||
RenderTreeFrameType.Component, 5678, 2000, 0,
|
||||
RenderTreeFrameType.ComponentReferenceCapture, 0, 0, 0,
|
||||
RenderTreeFrameType.Element, 1234, "Some element", 0,
|
||||
RenderTreeFrameType.ElementReferenceCapture, 12121, 0, 0,
|
||||
RenderTreeFrameType.ElementReferenceCapture, "my unique ID", 0, 0,
|
||||
RenderTreeFrameType.Region, 1234, 0, 0,
|
||||
RenderTreeFrameType.Text, "Some text", 0, 0
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue