-
-
Notifications
You must be signed in to change notification settings - Fork 7
feat(TcpSocket): add TcpSocket lib #512
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 19 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
489d284
feat: 增加 TouchSocket 扩展包
ArgoZhang e97d259
refactor: 重构 DefaultTcpSocketFactory 实现类
ArgoZhang 8b42331
refactor: 更新 DefaultTcpSocketClient 实现类
ArgoZhang 29298c9
refactor: 根据最新设计更改实现类
ArgoZhang 4681d4d
chore: 更新项目依赖关系
ArgoZhang 9f47065
feat: 根据新接口定义增加 ReceivedCallBack 属性
ArgoZhang b310fed
feat(tcp): 替换 DefaultTcpSocketClient 为 TouchSocketTcpClient
RRQM 3016e14
wip: 重构实现方法(未完成)
ArgoZhang bf3eb63
wip: 临时提交
ArgoZhang 2d218ec
Merge branch 'master' into feat-TouchSocket
ArgoZhang 1c5e7c3
test: 增加单元测试
ArgoZhang 9d2bea2
feat: 根据最新接口更新实现逻辑
ArgoZhang ab09440
refactor(DefaultTcpSocketClient): 优化 SendAsync 调用
RRQM 1a64858
refactor: 精简服务更新单元测试
ArgoZhang d691979
重构(tcp): 更新TouchSocket版本并优化连接逻辑
RRQM 819ca93
Merge branch 'master' into feat-TouchSocket
ArgoZhang 681d822
chore: 恢复项目引用
ArgoZhang bb7d38f
Merge branch 'master' into feat-TouchSocket
ArgoZhang e0cf878
chore: bump version 9.0.0
ArgoZhang 5cb3907
revert: 撤销其余项目更改
ArgoZhang File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
21 changes: 21 additions & 0 deletions
21
...mponents/BootstrapBlazor.OfficeDocumentViewer/BootstrapBlazor.OfficeDocumentViewer.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk.Razor"> | ||
|
|
||
| <PropertyGroup> | ||
| <Version>9.0.0</Version> | ||
| </PropertyGroup> | ||
|
|
||
| <PropertyGroup> | ||
| <PackageTags>Bootstrap Blazor WebAssembly wasm UI Components Office Viewer</PackageTags> | ||
| <Description>Bootstrap UI components extensions of Microsoft Office Documentation Viewer</Description> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="BootstrapBlazor" Version="$(BBVersion)" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <Using Include="BootstrapBlazor.Components" /> | ||
| <Using Include="Microsoft.JSInterop" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
5 changes: 5 additions & 0 deletions
5
src/components/BootstrapBlazor.OfficeDocumentViewer/OfficeDocumentViewer.razor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| @namespace BootstrapBlazor.Components | ||
| @inherits BootstrapModuleComponentBase | ||
| @attribute [JSModuleAutoLoader("./_content/BootstrapBlazor.OfficeDocumentViewer/OfficeDocumentViewer.razor.js", JSObjectReference = true, AutoInvokeDispose = false)] | ||
|
|
||
| <div @attributes="AdditionalAttributes" id="@Id" class="@ClassString" style="@StyleString"></div> |
103 changes: 103 additions & 0 deletions
103
src/components/BootstrapBlazor.OfficeDocumentViewer/OfficeDocumentViewer.razor.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| // Copyright (c) Argo Zhang (argo@163.com). All rights reserved. | ||
| // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
| // Website: https://www.blazor.zone or https://argozhang.github.io/ | ||
|
|
||
| using Microsoft.AspNetCore.Components; | ||
|
|
||
| namespace BootstrapBlazor.Components; | ||
|
|
||
| public partial class OfficeDocumentViewer | ||
| { | ||
| /// <summary> | ||
| /// Gets or sets the url for the PDF file to be displayed. | ||
| /// </summary> | ||
| [Parameter] | ||
| public string? Url { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the viewer height. Default is null. | ||
| /// </summary> | ||
| [Parameter] | ||
| public string? Height { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the document loaded event callback. | ||
| /// </summary> | ||
| [Parameter] | ||
| public Func<Task>? OnLoaded { get; set; } | ||
|
|
||
| [Inject, NotNull] | ||
| private NavigationManager? NavigationManager { get; set; } | ||
|
|
||
| private string? ClassString => CssBuilder.Default("bb-office-viewer-container") | ||
| .AddClassFromAttributes(AdditionalAttributes) | ||
| .Build(); | ||
|
|
||
| private string? StyleString => CssBuilder.Default() | ||
| .AddClass($"--bb-office-viewer-height: {Height};", !string.IsNullOrEmpty(Height)) | ||
| .Build(); | ||
|
|
||
| private string? _url; | ||
|
|
||
| /// <summary> | ||
| /// <inheritdoc/> | ||
| /// </summary> | ||
| /// <param name="firstRender"></param> | ||
| /// <returns></returns> | ||
| protected override async Task OnAfterRenderAsync(bool firstRender) | ||
| { | ||
| await base.OnAfterRenderAsync(firstRender); | ||
|
|
||
| if (firstRender) | ||
| { | ||
| _url = Url; | ||
| return; | ||
| } | ||
|
|
||
| var rerender = false; | ||
| if (_url != Url) | ||
| { | ||
| _url = Url; | ||
| rerender = true; | ||
| } | ||
|
|
||
| if (rerender) | ||
| { | ||
| await InvokeVoidAsync("load", Id, GetAbsoluteUri(_url)); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// <inheritdoc/> | ||
| /// </summary> | ||
| /// <returns></returns> | ||
| protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, new | ||
| { | ||
| LoadedCallaback = nameof(TriggerOnLoaded), | ||
| Url = GetAbsoluteUri(Url) | ||
| }); | ||
|
|
||
| private string GetAbsoluteUri(string? url) | ||
| { | ||
| url ??= string.Empty; | ||
| if (string.IsNullOrEmpty(url)) | ||
| { | ||
| return url; | ||
| } | ||
| var uri = NavigationManager.ToAbsoluteUri(url); | ||
| return uri.AbsoluteUri; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Trigger OnLoaded callback when the PDF document is loaded. | ||
| /// </summary> | ||
| /// <returns></returns> | ||
| [JSInvokable] | ||
| public async Task TriggerOnLoaded() | ||
| { | ||
| if (OnLoaded != null) | ||
| { | ||
| await OnLoaded(); | ||
| } | ||
| } | ||
| } |
37 changes: 37 additions & 0 deletions
37
src/components/BootstrapBlazor.OfficeDocumentViewer/OfficeDocumentViewer.razor.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import { addLink } from "../BootstrapBlazor/modules/utility.js" | ||
| import Data from "../BootstrapBlazor/modules/data.js" | ||
|
|
||
| export async function init(id, invoke, options) { | ||
| await addLink("./_content/BootstrapBlazor.OfficeDocumentViewer/office-viewer.css"); | ||
|
|
||
| const el = document.getElementById(id); | ||
| const officeViewer = { el, invoke, options }; | ||
| Data.set(id, officeViewer); | ||
|
|
||
| await load(id, options.url); | ||
| } | ||
|
|
||
| export async function load(id, url) { | ||
| const officeViewer = Data.get(id); | ||
| const { el, invoke, options } = officeViewer; | ||
|
|
||
| el.innerHTML = ''; | ||
|
|
||
| if (url) { | ||
| const { frame } = officeViewer; | ||
| const viewer = frame || createFrame(el); | ||
| if (options.loadedCallaback) { | ||
| viewer.onload = () => { | ||
| invoke.invokeMethodAsync(options.loadedCallaback); | ||
| }; | ||
| } | ||
| viewer.src = `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(url)}`; | ||
| } | ||
| } | ||
|
|
||
| const createFrame = el => { | ||
| const frame = document.createElement('iframe'); | ||
| frame.classList.add('bb-office-viewer'); | ||
| el.appendChild(frame); | ||
| return frame; | ||
| } | ||
2 changes: 2 additions & 0 deletions
2
src/components/BootstrapBlazor.OfficeDocumentViewer/_Imports.razor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| @using BootstrapBlazor.Components; | ||
| @using Microsoft.AspNetCore.Components.Web |
9 changes: 9 additions & 0 deletions
9
src/components/BootstrapBlazor.OfficeDocumentViewer/wwwroot/office-viewer.css
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| .bb-office-viewer-container { | ||
| width: 100%; | ||
| height: var(--bb-office-viewer-height, 500px); | ||
| } | ||
|
|
||
| .bb-office-viewer { | ||
| width: 100%; | ||
| height: 100%; | ||
| } |
2 changes: 1 addition & 1 deletion
2
src/extensions/BootstrapBlazor.Socket/BootstrapBlazor.Socket.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
src/extensions/BootstrapBlazor.TcpSocket/BootstrapBlazor.TcpSocket.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
32 changes: 32 additions & 0 deletions
32
src/extensions/BootstrapBlazor.TouchSocket/BootstrapBlazor.TouchSocket.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <Version>9.0.0</Version> | ||
| </PropertyGroup> | ||
|
|
||
| <PropertyGroup> | ||
| <PackageTags>Bootstrap Blazor WebAssembly wasm UI Components Topology FlowChart</PackageTags> | ||
| <Description>Bootstrap UI components extensions of FlowChart</Description> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <SupportedPlatform Remove="browser" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="TouchSocket" Version="4.0.0-Alpha.10" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <!--<PackageReference Include="BootstrapBlazor" Version="9.8.0-beta07" />--> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\..\..\..\BootstrapBlazor\src\BootstrapBlazor\BootstrapBlazor.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <Using Include="Microsoft.JSInterop" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
24 changes: 24 additions & 0 deletions
24
src/extensions/BootstrapBlazor.TouchSocket/Extensions/ServiceCollectionExtensions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| // Copyright (c) Argo Zhang (argo@163.com). All rights reserved. | ||
| // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
| // Website: https://www.blazor.zone or https://argozhang.github.io/ | ||
|
|
||
| using BootstrapBlazor.Components; | ||
|
|
||
| namespace Microsoft.Extensions.DependencyInjection; | ||
|
|
||
| /// <summary> | ||
| /// BootstrapBlazor 服务扩展类 | ||
| /// </summary> | ||
| public static class BootstrapBlazorTouchSocketServiceExtensions | ||
| { | ||
| /// <summary> | ||
| /// 添加 TouchSocket 服务 | ||
| /// </summary> | ||
| /// <param name="services"></param> | ||
| public static IServiceCollection AddBootstrapBlazorTouchSocketProviderService(this IServiceCollection services) | ||
| { | ||
| services.AddTransient<ISocketClientProvider, DefaultTcpSocketProvider>(); | ||
|
|
||
| return services; | ||
| } | ||
| } |
123 changes: 123 additions & 0 deletions
123
src/extensions/BootstrapBlazor.TouchSocket/Services/DefaultTcpSocketProvider.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,123 @@ | ||||||||||||||
| // Copyright (c) Argo Zhang (argo@163.com). All rights reserved. | ||||||||||||||
| // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||||||||||||||
| // Website: https://www.blazor.zone or https://argozhang.github.io/ | ||||||||||||||
|
|
||||||||||||||
| using System.Buffers; | ||||||||||||||
| using System.Diagnostics; | ||||||||||||||
| using System.IO.Pipelines; | ||||||||||||||
| using System.Net; | ||||||||||||||
| using TouchSocket.Core; | ||||||||||||||
| using TouchSocket.Sockets; | ||||||||||||||
|
|
||||||||||||||
| namespace BootstrapBlazor.Components; | ||||||||||||||
|
|
||||||||||||||
| internal sealed class DefaultTcpSocketProvider : TcpClientBase, ISocketClientProvider | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (complexity): Consider replacing the custom pipeline and semaphore logic with a simple NetworkStream to handle reading and writing.
Suggested change
await TcpConnectAsync(int.MaxValue, token);
_networkStream = new NetworkStream(MainSocket!, ownsSocket: true);
public async ValueTask<int> ReceiveAsync(Memory<byte> buffer, CancellationToken token = default)
{
token.ThrowIfCancellationRequested();
ThrowIfTcpClientNotConnected(); // unchanged
ThrowIfDisposed(); // unchanged
try
{
int bytesRead = await _networkStream!.ReadAsync(buffer, token);
return bytesRead;
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
Logger?.Exception(this, ex);
return 0;
}
}
public async ValueTask<bool> SendAsync(ReadOnlyMemory<byte> data, CancellationToken token = default)
{
token.ThrowIfCancellationRequested();
ThrowIfTcpClientNotConnected();
ThrowIfDisposed();
try
{
await _networkStream!.WriteAsync(data, token);
await _networkStream.FlushAsync(token);
return true;
}
catch (Exception ex)
{
Logger?.Exception(this, ex);
return false;
}
}
These changes maintain the same public API and semantics but drop most of the custom pipeline plumbing. |
||||||||||||||
| { | ||||||||||||||
| /// <summary> | ||||||||||||||
| /// <inheritdoc/> | ||||||||||||||
| /// </summary> | ||||||||||||||
| public bool IsConnected => base.Online; | ||||||||||||||
|
|
||||||||||||||
| /// <summary> | ||||||||||||||
| /// <inheritdoc/> | ||||||||||||||
| /// </summary> | ||||||||||||||
| public IPEndPoint LocalEndPoint { get; set; } = new IPEndPoint(IPAddress.Any, 0); | ||||||||||||||
|
|
||||||||||||||
| /// <summary> | ||||||||||||||
| /// <inheritdoc/> | ||||||||||||||
| /// </summary> | ||||||||||||||
| public async ValueTask CloseAsync() | ||||||||||||||
| { | ||||||||||||||
| await base.CloseAsync(string.Empty); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /// <summary> | ||||||||||||||
| /// <inheritdoc/> | ||||||||||||||
| /// </summary> | ||||||||||||||
| public async ValueTask<bool> ConnectAsync(IPEndPoint endPoint, CancellationToken token = default) | ||||||||||||||
| { | ||||||||||||||
| await SetupAsync(new TouchSocketConfig() | ||||||||||||||
| .SetBindIPHost(new IPHost(LocalEndPoint.Address, LocalEndPoint.Port)) | ||||||||||||||
| .SetRemoteIPHost(new IPHost(endPoint.Address, endPoint.Port))); | ||||||||||||||
|
|
||||||||||||||
| try | ||||||||||||||
| { | ||||||||||||||
| await TcpConnectAsync(int.MaxValue, token); | ||||||||||||||
| Debug.Assert(MainSocket != null, "MainSocket cannot be null after connection."); | ||||||||||||||
| Debug.Assert(base.Online, "Online should be true after successful connection."); | ||||||||||||||
| if (MainSocket.LocalEndPoint is IPEndPoint localEndPoint) | ||||||||||||||
| { | ||||||||||||||
| LocalEndPoint = localEndPoint; | ||||||||||||||
| } | ||||||||||||||
| return true; | ||||||||||||||
| } | ||||||||||||||
| catch (Exception ex) | ||||||||||||||
| { | ||||||||||||||
| this.Logger?.Exception(this, ex); | ||||||||||||||
| return false; | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /// <summary> | ||||||||||||||
| /// <inheritdoc/> | ||||||||||||||
| /// </summary> | ||||||||||||||
| public async ValueTask<int> ReceiveAsync(Memory<byte> buffer, CancellationToken token = default) | ||||||||||||||
| { | ||||||||||||||
| token.ThrowIfCancellationRequested(); | ||||||||||||||
| this.ThrowIfTcpClientNotConnected(); | ||||||||||||||
| this.ThrowIfDisposed(); | ||||||||||||||
|
|
||||||||||||||
| var result = await base.Transport.Input.ReadAsync(token); | ||||||||||||||
| if (result.IsCompleted) | ||||||||||||||
| { | ||||||||||||||
| return 0; | ||||||||||||||
| } | ||||||||||||||
| var length = (int)Math.Min(result.Buffer.Length, buffer.Length); | ||||||||||||||
|
|
||||||||||||||
| var sequence = result.Buffer.Slice(0, length); | ||||||||||||||
|
|
||||||||||||||
| sequence.CopyTo(buffer.Span); | ||||||||||||||
| base.Transport.Input.AdvanceTo(sequence.End); | ||||||||||||||
| return length; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /// <summary> | ||||||||||||||
| /// <inheritdoc/> | ||||||||||||||
| /// </summary> | ||||||||||||||
| public async ValueTask<bool> SendAsync(ReadOnlyMemory<byte> data, CancellationToken token = default) | ||||||||||||||
| { | ||||||||||||||
| token.ThrowIfCancellationRequested(); | ||||||||||||||
| base.ThrowIfTcpClientNotConnected(); | ||||||||||||||
| base.ThrowIfDisposed(); | ||||||||||||||
| var pipeWriter = base.Transport.Output; | ||||||||||||||
| var locker = base.Transport.SemaphoreSlimForWriter; | ||||||||||||||
| await locker.WaitAsync(token); | ||||||||||||||
| try | ||||||||||||||
| { | ||||||||||||||
| pipeWriter.Write(data.Span); | ||||||||||||||
| var result = await pipeWriter.FlushAsync(token); | ||||||||||||||
| if (result.IsCanceled || result.IsCompleted) | ||||||||||||||
| { | ||||||||||||||
| return false; | ||||||||||||||
| } | ||||||||||||||
| return true; | ||||||||||||||
| } | ||||||||||||||
| catch (Exception ex) | ||||||||||||||
| { | ||||||||||||||
| this.Logger?.Exception(this, ex); | ||||||||||||||
| return false; | ||||||||||||||
| } | ||||||||||||||
| finally | ||||||||||||||
| { | ||||||||||||||
| locker.Release(); | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| protected override sealed async Task ReceiveLoopAsync(ITransport transport) | ||||||||||||||
| { | ||||||||||||||
| //重写接收循环方法 | ||||||||||||||
| //此处不做任何数据读取 | ||||||||||||||
| //让数据直接到ReceiveAsync使用管道直接读取数据 | ||||||||||||||
| await Task.Delay(-1, transport.ClosedToken); | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (typo): Typo in property access: 'loadedCallaback' should be 'loadedCallback'.
This typo could cause runtime errors due to referencing a non-existent property.