Skip to content

Commit

Permalink
feat(IConnectionService): add IConnectionService (#3121)
Browse files Browse the repository at this point in the history
* refactor: 增加指纹方法

* refactor: 更新浏览器指纹组件

* refactor: 更新 WebClientService 脚本

* feat: 增加连接服务

* feat: 增加 CollectionHub 组件

* refactor: 集成连接器组件

* test: 增加单元测试

* refactor: 更新脚本

* refactor: 增加扫描周期逻辑

* feat: 增加 ConfigureCollectionHubOptions 方法

* doc: 更新 Console 文档

* refactor: 重构缓存类型

* feat: 重构 CollectionHubOptions 配置类

* test: 更新单元测试

* test: 更新单元测试

* test: 更新单元测试

* test: 更新单元测试

* test: 更新单元测试
  • Loading branch information
ArgoZhang authored Mar 23, 2024
1 parent 44abb4e commit de5ff9c
Show file tree
Hide file tree
Showing 15 changed files with 321 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,14 @@ private static AttributeItem[] GetAttributes() =>
ValueList = " — ",
DefaultValue = " — "
},
new()
{
Name = "ItemTemplate",
Description = "Item 模板",
Type = "RenderFragment",
ValueList = " — ",
DefaultValue = " — "
},
new(){
Name = "LightTitle",
Description = "指示灯 Title",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

<ResizeNotification></ResizeNotification>
<BrowserFinger></BrowserFinger>
<ConnectionHub></ConnectionHub>

@code {
RenderFragment RenderChildContent =>
Expand Down
32 changes: 32 additions & 0 deletions src/BootstrapBlazor/Components/ConnectionHub/CollectionItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// 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/

namespace BootstrapBlazor.Components;

/// <summary>
/// 连接对象实体类
/// </summary>
public class CollectionItem
{
/// <summary>
/// 获得/设置 连接 Id
/// </summary>
[NotNull]
public string? Id { get; internal set; }

/// <summary>
/// 获得/设置 连接 Ip 地址
/// </summary>
public ClientInfo? ClientInfo { get; set; }

/// <summary>
/// 获得/设置 开始连接时间
/// </summary>
public DateTimeOffset ConnectionTime { get; internal set; }

/// <summary>
/// 获得/设置 上次心跳时间
/// </summary>
public DateTimeOffset LastBeatTime { get; internal set; }
}
37 changes: 37 additions & 0 deletions src/BootstrapBlazor/Components/ConnectionHub/ConnectionHub.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// 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/

namespace BootstrapBlazor.Components;

/// <summary>
/// 客户端链接组件
/// </summary>
[BootstrapModuleAutoLoader(ModuleName = "hub", JSObjectReference = true, AutoInvokeInit = true, AutoInvokeDispose = false)]
public class ConnectionHub : BootstrapModuleComponentBase
{
[Inject]
[NotNull]
private IConnectionService? ConnectionService { get; set; }

/// <summary>
/// <inheritdoc/>
/// </summary>
/// <returns></returns>
protected override Task InvokeInitAsync() => InvokeVoidAsync("init", new { Invoke = Interop, Method = nameof(Callback) });

/// <summary>
/// JSInvoke 回调方法
/// </summary>
/// <param name="client"></param>
/// <returns></returns>
[JSInvokable]
public Task Callback(ClientInfo client)
{
if (!string.IsNullOrEmpty(client.Id))
{
ConnectionService.AddOrUpdate(client);
}
return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ public static IServiceCollection AddBootstrapBlazor(this IServiceCollection serv
// 假日服务
services.TryAddSingleton<ICalendarHolidays, DefaultCalendarHolidays>();

// 在线连接服务
services.TryAddSingleton<IConnectionService, DefaultConnectionService>();

services.TryAddScoped(typeof(IDataService<>), typeof(NullDataService<>));
services.TryAddScoped<IReconnectorProvider, ReconnectorProvider>();
services.TryAddScoped<IGeoLocationService, DefaultGeoLocationService>();
Expand Down Expand Up @@ -141,7 +144,6 @@ public static IServiceCollection ConfigureIPLocatorOption(this IServiceCollectio
/// </summary>
/// <param name="services"></param>
/// <param name="localizationConfigure"></param>
/// <returns></returns>
public static IServiceCollection ConfigureJsonLocalizationOptions(this IServiceCollection services, Action<JsonLocalizationOptions> localizationConfigure)
{
services.Configure(localizationConfigure);
Expand All @@ -153,7 +155,6 @@ public static IServiceCollection ConfigureJsonLocalizationOptions(this IServiceC
/// </summary>
/// <typeparam name="TOptions"></typeparam>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddOptionsMonitor<TOptions>(this IServiceCollection services) where TOptions : class
{
services.AddOptions();
Expand Down
5 changes: 5 additions & 0 deletions src/BootstrapBlazor/Options/BootstrapBlazorOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ public class BootstrapBlazorOptions
/// </summary>
public string? JSModuleVersion { get; set; }

/// <summary>
/// 获得/设置 CollectionHubOptions 配置 默认为 null
/// </summary>
public CollectionHubOptions? CollectionHubOptions { get; set; }

/// <summary>
/// 获得支持多语言集合
/// </summary>
Expand Down
21 changes: 21 additions & 0 deletions src/BootstrapBlazor/Options/CollectionHubOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// 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/

namespace BootstrapBlazor.Components;

/// <summary>
/// CollectionHub 配置类
/// </summary>
public class CollectionHubOptions
{
/// <summary>
/// 获得/设置 过期扫描周期 默认 30秒
/// </summary>
public TimeSpan ExpirationScanFrequency { get; set; } = TimeSpan.FromSeconds(30);

/// <summary>
/// 获得/设置 ConnectionHub 组件心跳间隔
/// </summary>
public int BeatInterval { get; set; } = 5000;
}
103 changes: 103 additions & 0 deletions src/BootstrapBlazor/Services/DefaultConnectionService.cs
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 System.Collections.Concurrent;

namespace BootstrapBlazor.Components;

/// <summary>
/// 当前链接服务
/// </summary>
class DefaultConnectionService : IConnectionService, IDisposable
{
private readonly ConcurrentDictionary<string, CollectionItem> _connectionCache = new();

private readonly CollectionHubOptions _options = default!;

private readonly CancellationTokenSource _cancellationTokenSource = new();

public DefaultConnectionService(IOptions<BootstrapBlazorOptions> options)
{
_options = options.Value.CollectionHubOptions ?? new CollectionHubOptions();

Task.Run(() =>
{
while (!_cancellationTokenSource.IsCancellationRequested)
{
try
{
Task.Delay(_options.ExpirationScanFrequency, _cancellationTokenSource.Token);

var keys = _connectionCache.Values.Where(i => i.LastBeatTime.AddMilliseconds(_options.BeatInterval) < DateTimeOffset.Now).Select(i => i.Id).ToList();
keys.ForEach(i => _connectionCache.TryRemove(i, out _));
}
catch { }
}
});
}

/// <summary>
/// <inheritdoc/>
/// </summary>
public long Count => _connectionCache.Values.LongCount(i => i.LastBeatTime.AddMilliseconds(_options.BeatInterval) > DateTimeOffset.Now);

/// <summary>
/// <inheritdoc/>
/// </summary>
/// <param name="client"></param>
public void AddOrUpdate(ClientInfo client)
{
if (!string.IsNullOrEmpty(client.Id))
{
_connectionCache.AddOrUpdate(client.Id, key => CreateItem(key, client), (k, v) => UpdateItem(v, client));
}
}

private static CollectionItem CreateItem(string key, ClientInfo client)
{
return new CollectionItem()
{
Id = key,
ConnectionTime = DateTimeOffset.Now,
LastBeatTime = DateTimeOffset.Now,
ClientInfo = client
};
}

private static CollectionItem UpdateItem(CollectionItem item, ClientInfo val)
{
item.LastBeatTime = DateTimeOffset.Now;
item.ClientInfo = val;
return item;
}

/// <summary>
/// <inheritdoc/>
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
/// <returns></returns>
public bool TryGetValue(string key, [MaybeNullWhen(false)] out CollectionItem? value) => _connectionCache.TryGetValue(key, out value);

private void Dispose(bool disposing)
{
if (disposing)
{
if (!_cancellationTokenSource.IsCancellationRequested)
{
_cancellationTokenSource.Cancel();
_cancellationTokenSource.Dispose();
}
}
}

/// <summary>
/// <inheritdoc/>
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
30 changes: 30 additions & 0 deletions src/BootstrapBlazor/Services/IConnectionService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// 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/

namespace BootstrapBlazor.Components;

/// <summary>
/// 当前链接服务
/// </summary>
public interface IConnectionService
{
/// <summary>
/// 增加或更新当前 Key
/// </summary>
/// <param name="client">ClientInfo 实例</param>
void AddOrUpdate(ClientInfo client);

/// <summary>
/// 获得指定 key 的连接信息
/// </summary>
/// <param name="key">键值</param>
/// <param name="value">连接信息</param>
/// <returns></returns>
bool TryGetValue(string key, out CollectionItem? value);

/// <summary>
/// 获得在线连接数
/// </summary>
long Count { get; }
}
27 changes: 27 additions & 0 deletions src/BootstrapBlazor/wwwroot/modules/hub.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import "./browser.js?v=$version"
import { execute } from "./ajax.js?v=$version"
import { getFingerCode } from "./utility.js?v=$version"

export async function init(options) {
const { invoke, method, interval = 3000 } = options;
const info = browser()
let data = {
browser: info.browser + ' ' + info.version,
device: info.device,
language: info.language,
engine: info.engine,
userAgent: navigator.userAgent,
os: info.system + ' ' + info.systemVersion
}
const result = await execute({
method: 'GET',
url: './ip.axd'
});
const code = getFingerCode();
data.id = code;
data.ip = result.ip;

setTimeout(() => {
invoke.invokeMethodAsync(method, data);
}, interval);
}
44 changes: 44 additions & 0 deletions test/UnitTest/Components/ConnectionHubTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// 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.Extensions.DependencyInjection;

namespace UnitTest.Components;

public class ConnectionHubTest : BootstrapBlazorTestBase
{
[Fact]
public async Task AddConnection_Ok()
{
var mockData = new ClientInfo()
{
Id = "test_id",
Ip = "192.168.0.1",
OS = "ios",
Browser = "chrome",
Device = WebClientDeviceType.Mobile,
Language = "zh",
Engine = "engine",
UserAgent = "test_agent"
};

var service = Context.Services.GetRequiredService<IConnectionService>();
var cut = Context.RenderComponent<ConnectionHub>();
await cut.InvokeAsync(() =>
{
cut.Instance.Callback(mockData);
});
Assert.Equal(1, service.Count);

// 触发 Beat 时间
await Task.Delay(10);
await cut.InvokeAsync(() =>
{
cut.Instance.Callback(mockData);
});
Assert.True(service.TryGetValue(mockData.Id, out var item));
Assert.NotNull(item?.ClientInfo);
Assert.True(item?.ConnectionTime < DateTimeOffset.Now);
}
}
2 changes: 1 addition & 1 deletion test/UnitTest/Components/CountButtonTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ await cut.InvokeAsync(() =>
await Task.Delay(500);
Assert.Contains("(1) DisplayText", cut.Markup);

await Task.Delay(600);
await Task.Delay(700);
Assert.DoesNotContain("disabled=\"disabled\"", cut.Markup);
Assert.Contains("DisplayText", cut.Markup);

Expand Down
6 changes: 4 additions & 2 deletions test/UnitTest/Components/TimerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,10 @@ public async Task OnStart_Ok()
});
});
var downs = cut.FindAll(".time-spinner-arrow.fa-angle-down");
downs[2].Click();
cut.Find(".time-panel-btn.confirm").Click();
await cut.InvokeAsync(() => downs[2].Click());

var confirm = cut.Find(".time-panel-btn.confirm");
await cut.InvokeAsync(() => confirm.Click());

await Task.Delay(2000);
Assert.True(timeout);
Expand Down
Loading

0 comments on commit de5ff9c

Please sign in to comment.