Compare commits
66
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba664b95b8 | ||
|
|
d6f370f862 | ||
|
|
079826f4de | ||
|
|
2eee3cdaab | ||
|
|
5e3215ee52 | ||
|
|
7739cc2f3d | ||
|
|
8347a7c126 | ||
|
|
fbfc960df6 | ||
|
|
451f721030 | ||
|
|
bb992c0410 | ||
|
|
db18aebf92 | ||
|
|
8127c9c9fe | ||
|
|
1825b339b1 | ||
|
|
03ab3fbe90 | ||
|
|
b61bf16a42 | ||
|
|
d969489bf4 | ||
|
|
9cd4974f49 | ||
|
|
793634c8cb | ||
|
|
eae20b6c94 | ||
|
|
2848bb6a5d | ||
|
|
687324998b | ||
|
|
97cebd62d4 | ||
|
|
492ca0c28f | ||
|
|
baa5019039 | ||
|
|
ac2d31e1fc | ||
|
|
9821134bc3 | ||
|
|
9cf892322d | ||
|
|
77bf91be1e | ||
|
|
63b064ac79 | ||
|
|
b617775c29 | ||
|
|
4f7d77420c | ||
|
|
3ba6a91ca7 | ||
|
|
0abde9eb7c | ||
|
|
1531a57bc5 | ||
|
|
a62ea64c5e | ||
|
|
450773f588 | ||
|
|
f09d6badaf | ||
|
|
6b054e0718 | ||
|
|
5e2789d16e | ||
|
|
9ebdb2e460 | ||
|
|
d7bf82722b | ||
|
|
e1526e25a7 | ||
|
|
089ba9b369 | ||
|
|
567976043f | ||
|
|
2fd3382939 | ||
|
|
a96f1c8f17 | ||
|
|
6f623f9a37 | ||
|
|
827189cdb4 | ||
|
|
123f12ab88 | ||
|
|
f51ba8fb56 | ||
|
|
2506fb4f2b | ||
|
|
f92964114e | ||
|
|
baa0f1430b | ||
|
|
7104bd787a | ||
|
|
5db92d1f7d | ||
|
|
eb0fcf0b84 | ||
|
|
a52b187313 | ||
|
|
669b059eb2 | ||
|
|
36ab8492d3 | ||
|
|
54f37b2401 | ||
|
|
5cf7eacd82 | ||
|
|
087d8ab8c9 | ||
|
|
b69b7b3350 | ||
|
|
5620f33524 | ||
|
|
f3e33fa390 | ||
|
|
3b631e0d5f |
No files matched your search
@@ -66,6 +66,7 @@ RUN \
|
||||
|
||||
# Dotnet
|
||||
RUN apt-get update && apt-get install -y dotnet7
|
||||
RUN apt-get update && apt-get install -y dotnet8
|
||||
|
||||
# Node
|
||||
ARG node_major=18
|
||||
|
||||
@@ -19,6 +19,14 @@
|
||||
"--shm-size",
|
||||
"2gb",
|
||||
"-p",
|
||||
"5000:5000",
|
||||
"-p",
|
||||
"5001:5001",
|
||||
"-p",
|
||||
"5173:5173",
|
||||
"-p",
|
||||
"5174:5174",
|
||||
"-p",
|
||||
"8000:8000",
|
||||
"-p",
|
||||
"8001:8001",
|
||||
@@ -47,4 +55,4 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
-16
@@ -207,17 +207,23 @@ dotnet_naming_symbols.parameters.applicable_accessibilities = public,internal,pr
|
||||
dotnet_naming_symbols.private_symbols.applicable_kinds = property,method,field,event,delegate
|
||||
dotnet_naming_symbols.private_symbols.applicable_accessibilities = protected,private
|
||||
|
||||
# local symbols
|
||||
dotnet_naming_symbols.local_symbols.applicable_kinds = local
|
||||
|
||||
# parameter symbols
|
||||
dotnet_naming_symbols.parameter_symbols.applicable_kinds = parameter
|
||||
|
||||
# Naming styles
|
||||
|
||||
dotnet_naming_style.snake_case.required_prefix =
|
||||
dotnet_naming_style.snake_case.required_suffix =
|
||||
dotnet_naming_style.snake_case.word_separator = _
|
||||
dotnet_naming_style.snake_case.capitalization = all_lower
|
||||
dotnet_naming_style.pascal_case.required_prefix =
|
||||
dotnet_naming_style.pascal_case.required_suffix =
|
||||
dotnet_naming_style.pascal_case.word_separator =
|
||||
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.private_snake_case.required_prefix = _
|
||||
dotnet_naming_style.private_snake_case.required_suffix =
|
||||
dotnet_naming_style.private_snake_case.word_separator = _
|
||||
dotnet_naming_style.private_snake_case.capitalization = all_lower
|
||||
dotnet_naming_style.camel_case.required_prefix =
|
||||
dotnet_naming_style.camel_case.required_suffix =
|
||||
dotnet_naming_style.camel_case.word_separator =
|
||||
dotnet_naming_style.camel_case.capitalization = camel_case
|
||||
|
||||
dotnet_naming_style.user_types.required_prefix =
|
||||
dotnet_naming_style.user_types.required_suffix =
|
||||
@@ -229,6 +235,10 @@ dotnet_naming_style.interface.required_suffix =
|
||||
dotnet_naming_style.interface.word_separator =
|
||||
dotnet_naming_style.interface.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.local.capitalization = camel_case
|
||||
|
||||
dotnet_naming_style.parameter.capitalization = camel_case
|
||||
|
||||
# rules
|
||||
dotnet_naming_rule.user_types.symbols = user_types
|
||||
dotnet_naming_rule.user_types.style = user_types
|
||||
@@ -238,14 +248,18 @@ dotnet_naming_rule.interfaces.symbols = interface
|
||||
dotnet_naming_rule.interfaces.style = interface_style
|
||||
dotnet_naming_rule.interfaces.severity = suggestion
|
||||
|
||||
dotnet_naming_rule.public_members_snake_case.symbols = public_symbols
|
||||
dotnet_naming_rule.public_members_snake_case.style = snake_case
|
||||
dotnet_naming_rule.public_members_snake_case.severity = suggestion
|
||||
dotnet_naming_rule.public_members.symbols = public_symbols
|
||||
dotnet_naming_rule.public_members.style = pascal_case
|
||||
dotnet_naming_rule.public_members.severity = suggestion
|
||||
|
||||
dotnet_naming_rule.private_members_snake_case.symbols = private_symbols
|
||||
dotnet_naming_rule.private_members_snake_case.style = private_snake_case
|
||||
dotnet_naming_rule.private_members_snake_case.severity = suggestion
|
||||
dotnet_naming_rule.private_members.symbols = private_symbols
|
||||
dotnet_naming_rule.private_members.style = camel_case
|
||||
dotnet_naming_rule.private_members.severity = suggestion
|
||||
|
||||
dotnet_naming_rule.parameters.symbols = parameters
|
||||
dotnet_naming_rule.parameters.style = snake_case
|
||||
dotnet_naming_rule.variables_are_camel_case.severity = suggestion
|
||||
dotnet_naming_rule.variables_are_camel_case.symbols = local_symbols
|
||||
dotnet_naming_rule.variables_are_camel_case.style = local
|
||||
|
||||
dotnet_naming_rule.parameters.symbols = parameter_symbols
|
||||
dotnet_naming_rule.parameters.style = parameter
|
||||
dotnet_naming_rule.parameters.severity = suggestion
|
||||
+3
-3
@@ -1,3 +1,3 @@
|
||||
*.cmd eol=crlf
|
||||
*.cs eol=crlf
|
||||
*.sh eol=lf
|
||||
*.cmd text=auto eol=crlf
|
||||
*.cs text=auto eol=crlf
|
||||
*.sh text=auto eol=lf
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[submodule "externals"]
|
||||
path = externals
|
||||
url = git@bitbucket.org:bluetradesys/externals.git
|
||||
url = https://bitbucket.org/bluetradesys/externals.git
|
||||
[submodule "vcpkg"]
|
||||
path = vcpkg
|
||||
url = https://github.com/microsoft/vcpkg.git
|
||||
Vendored
+69
-2
@@ -6,6 +6,35 @@
|
||||
"type": "coreclr",
|
||||
"request": "attach"
|
||||
},
|
||||
{
|
||||
"name": "Bts.DataStreamWebApp.Server",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "Build DataStream WebApp (Debug)",
|
||||
"program": "${workspaceFolder}/build/Debug/bin/net7.0/Bts.DataStreamWebApp.Server.dll",
|
||||
"args": [
|
||||
"--config",
|
||||
"${workspaceFolder}/src/DataStreamSuite/Bts.DataStreamWebApp.Server/Configurations/DevServer.json",
|
||||
],
|
||||
"env": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy",
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Bts.DataStreamWebApp.Server (Skip Build)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/build/Debug/bin/net7.0/Bts.DataStreamWebApp.Server.dll",
|
||||
"args": [
|
||||
"--config",
|
||||
"${workspaceFolder}/src/DataStreamSuite/Bts.DataStreamWebApp.Server/Configurations/DevServer.json",
|
||||
],
|
||||
"env": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy",
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "DataStreamPublisher",
|
||||
"type": "coreclr",
|
||||
@@ -22,7 +51,26 @@
|
||||
"--config",
|
||||
"${workspaceFolder}/src/DataStreamSuite/DataStreamPublisher/Configurations/${input:dataStreamPublisherConfig}.json"
|
||||
],
|
||||
"cwd": "build/Debug/bin/net7.0",
|
||||
"cwd": "${workspaceFolder}/build/Debug/bin/net7.0",
|
||||
"stopAtEntry": false,
|
||||
"console": "integratedTerminal"
|
||||
},
|
||||
{
|
||||
"name": "DataStreamPublisher (Skip Build)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "",
|
||||
"linux": {
|
||||
"program": "DataStreamPublisher",
|
||||
},
|
||||
"windows": {
|
||||
"program": "DataStreamPublisher.exe",
|
||||
},
|
||||
"args": [
|
||||
"--config",
|
||||
"${workspaceFolder}/src/DataStreamSuite/DataStreamPublisher/Configurations/${input:dataStreamPublisherConfig}.json"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/build/Debug/bin/net7.0",
|
||||
"stopAtEntry": false,
|
||||
"console": "integratedTerminal"
|
||||
},
|
||||
@@ -30,7 +78,7 @@
|
||||
"name": "DataStreamQueryAnalyzer",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "Build DataStream (Debug)",
|
||||
"preLaunchTask": "Build DataStreamQueryAnalyzer (Debug)",
|
||||
"program": "",
|
||||
"windows": {
|
||||
"program": "DataStreamQueryAnalyzer.exe",
|
||||
@@ -66,6 +114,25 @@
|
||||
"stopAtEntry": false,
|
||||
"console": "integratedTerminal"
|
||||
},
|
||||
{
|
||||
"name": "PubSubServer (Skip Build)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "",
|
||||
"linux": {
|
||||
"program": "PubSubServer",
|
||||
},
|
||||
"windows": {
|
||||
"program": "PubSubServer.exe",
|
||||
},
|
||||
"args": [
|
||||
"--config",
|
||||
"${workspaceFolder}/src/DataStreamSuite/PubSubServer/Configurations/${input:pubSubServerConfig}.json"
|
||||
],
|
||||
"cwd": "build/Debug/bin/net7.0",
|
||||
"stopAtEntry": false,
|
||||
"console": "integratedTerminal"
|
||||
},
|
||||
{
|
||||
"name": "Spark",
|
||||
"type": "coreclr",
|
||||
|
||||
Vendored
+184
-3
@@ -121,6 +121,97 @@
|
||||
"color": "terminal.ansiCyan"
|
||||
},
|
||||
},
|
||||
{
|
||||
"label": "Build DataStreamQueryAnalyzer",
|
||||
"group": "build",
|
||||
"type": "process",
|
||||
"command": "",
|
||||
"windows": {
|
||||
"command": "tools/build_data_stream_query_analyzer.cmd",
|
||||
},
|
||||
"args": [
|
||||
"--configuration",
|
||||
"${input:buildConfiguration}",
|
||||
],
|
||||
"icon": {
|
||||
"id": "terminal",
|
||||
"color": "terminal.ansiCyan"
|
||||
},
|
||||
},
|
||||
{
|
||||
"label": "Build DataStreamQueryAnalyzer (Debug)",
|
||||
"group": "build",
|
||||
"type": "process",
|
||||
"command": "",
|
||||
"windows": {
|
||||
"command": "tools/build_data_stream_query_analyzer.cmd",
|
||||
},
|
||||
"args": [
|
||||
"--configuration",
|
||||
"Debug",
|
||||
],
|
||||
"icon": {
|
||||
"id": "terminal",
|
||||
"color": "terminal.ansiCyan"
|
||||
},
|
||||
},
|
||||
{
|
||||
"label": "Build DataStreamQueryAnalyzer (Release)",
|
||||
"group": "build",
|
||||
"type": "process",
|
||||
"command": "",
|
||||
"windows": {
|
||||
"command": "tools/build_data_stream_query_analyzer.cmd",
|
||||
},
|
||||
"args": [
|
||||
"--configuration",
|
||||
"Release",
|
||||
],
|
||||
"icon": {
|
||||
"id": "terminal",
|
||||
"color": "terminal.ansiCyan"
|
||||
},
|
||||
},
|
||||
{
|
||||
"label": "Build DataStream WebApp",
|
||||
"group": "build",
|
||||
"type": "process",
|
||||
"command": "",
|
||||
"linux": {
|
||||
"command": "tools/build_data_stream_web_app.sh",
|
||||
},
|
||||
"windows": {
|
||||
"command": "tools/build_data_stream_web_app.cmd",
|
||||
},
|
||||
"args": [
|
||||
"--configuration",
|
||||
"${input:buildConfiguration}",
|
||||
],
|
||||
"icon": {
|
||||
"id": "terminal",
|
||||
"color": "terminal.ansiCyan"
|
||||
},
|
||||
},
|
||||
{
|
||||
"label": "Build DataStream WebApp (Debug)",
|
||||
"group": "build",
|
||||
"type": "process",
|
||||
"command": "",
|
||||
"linux": {
|
||||
"command": "tools/build_data_stream_web_app.sh",
|
||||
},
|
||||
"windows": {
|
||||
"command": "tools/build_data_stream_web_app.cmd",
|
||||
},
|
||||
"args": [
|
||||
"--configuration",
|
||||
"Debug",
|
||||
],
|
||||
"icon": {
|
||||
"id": "terminal",
|
||||
"color": "terminal.ansiCyan"
|
||||
},
|
||||
},
|
||||
{
|
||||
"label": "Build Spark",
|
||||
"group": "build",
|
||||
@@ -303,6 +394,22 @@
|
||||
"color": "terminal.ansiCyan"
|
||||
},
|
||||
},
|
||||
{
|
||||
"label": "Publish DataStreamWebApp",
|
||||
"group": "build",
|
||||
"type": "process",
|
||||
"command": "",
|
||||
"linux": {
|
||||
"command": "tools/publish_data_stream_web_app.sh",
|
||||
},
|
||||
"windows": {
|
||||
"command": "tools/publish_data_stream_web_app.cmd",
|
||||
},
|
||||
"icon": {
|
||||
"id": "terminal",
|
||||
"color": "terminal.ansiCyan"
|
||||
},
|
||||
},
|
||||
{
|
||||
"label": "Edit DataStream Config",
|
||||
"type": "process",
|
||||
@@ -353,7 +460,7 @@
|
||||
},
|
||||
{
|
||||
"label": "Run DataStreamQueryAnalyzer",
|
||||
"dependsOn": "Build DataStream",
|
||||
"dependsOn": "Build DataStreamQueryAnalyzer",
|
||||
"type": "process",
|
||||
"command": "",
|
||||
"windows": {
|
||||
@@ -373,7 +480,7 @@
|
||||
},
|
||||
{
|
||||
"label": "Run DataStreamQueryAnalyzer (Debug)",
|
||||
"dependsOn": "Build DataStream (Debug)",
|
||||
"dependsOn": "Build DataStreamQueryAnalyzer (Debug)",
|
||||
"type": "process",
|
||||
"command": "",
|
||||
"windows": {
|
||||
@@ -393,7 +500,7 @@
|
||||
},
|
||||
{
|
||||
"label": "Run DataStreamQueryAnalyzer (Release)",
|
||||
"dependsOn": "Build DataStream (Release)",
|
||||
"dependsOn": "Build DataStreamQueryAnalyzer (Release)",
|
||||
"type": "process",
|
||||
"command": "",
|
||||
"windows": {
|
||||
@@ -526,6 +633,28 @@
|
||||
"color": "terminal.ansiGreen",
|
||||
},
|
||||
},
|
||||
{
|
||||
"label": "Run PubSubServer (Debug | Skip Build)",
|
||||
"type": "process",
|
||||
"command": "",
|
||||
"linux": {
|
||||
"command": "PubSubServer",
|
||||
},
|
||||
"windows": {
|
||||
"command": "PubSubServer.exe",
|
||||
},
|
||||
"args": [
|
||||
"--config",
|
||||
"${workspaceFolder}/src/DataStreamSuite/PubSubServer/Configurations/${input:pubSubServerConfig}.json",
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/build/Debug/bin/net7.0"
|
||||
},
|
||||
"icon": {
|
||||
"id": "run",
|
||||
"color": "terminal.ansiGreen",
|
||||
},
|
||||
},
|
||||
{
|
||||
"label": "Run PubSubServer (Release)",
|
||||
"dependsOn": "Build DataStream (Release)",
|
||||
@@ -549,6 +678,28 @@
|
||||
"color": "terminal.ansiGreen",
|
||||
},
|
||||
},
|
||||
{
|
||||
"label": "Run PubSubServer (Release | Skip Build)",
|
||||
"type": "process",
|
||||
"command": "",
|
||||
"linux": {
|
||||
"command": "PubSubServer",
|
||||
},
|
||||
"windows": {
|
||||
"command": "PubSubServer.exe",
|
||||
},
|
||||
"args": [
|
||||
"--config",
|
||||
"${workspaceFolder}/src/DataStreamSuite/PubSubServer/Configurations/${input:pubSubServerConfig}.json",
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/build/Release/bin/net7.0"
|
||||
},
|
||||
"icon": {
|
||||
"id": "run",
|
||||
"color": "terminal.ansiGreen",
|
||||
},
|
||||
},
|
||||
{
|
||||
"label": "Run Spark",
|
||||
"dependsOn": "Build Spark",
|
||||
@@ -684,6 +835,36 @@
|
||||
"id": "wrench"
|
||||
},
|
||||
},
|
||||
{
|
||||
"label": "View PubSubServer Logs (Last Run)",
|
||||
"type": "process",
|
||||
"command": "",
|
||||
"linux": {
|
||||
"command": "tools/logging/show_last_run.sh",
|
||||
"args": [
|
||||
"${env:HOME}/.local/share/DataStream/data/logging.PubSubServer",
|
||||
"Starting PubSubServer"
|
||||
]
|
||||
},
|
||||
"windows": {
|
||||
"command": "tools/logging/show_last_run.cmd",
|
||||
"args": [
|
||||
"DataStream",
|
||||
"PubSubServer.exe",
|
||||
"Starting PubSubServer"
|
||||
]
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"focus": true,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": false,
|
||||
"close": true,
|
||||
},
|
||||
"icon": {
|
||||
"id": "wrench"
|
||||
},
|
||||
},
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
|
||||
+3
-3
@@ -31,8 +31,7 @@
|
||||
"CMAKE_TOOLCHAIN_FILE": "${sourceDir}/vcpkg/scripts/buildsystems/vcpkg.cmake",
|
||||
"EXTERNALS_ROOT": "${sourceDir}/externals/windows",
|
||||
"CONFIG_DEFAULTS_PATH": "config/profiles/default/*.conf",
|
||||
"DOTNET_BINARY_DIR": "${sourceDir}/build/$<CONFIG>/bin/net7.0",
|
||||
"DOTNET_WINDOWS_BINARY_DIR": "${sourceDir}/build/$<CONFIG>/bin/net7.0-windows",
|
||||
"DOTNET_BINARY_DIR": "${sourceDir}/build/$<CONFIG>/bin/net7.0;${sourceDir}/build/$<CONFIG>/bin/net7.0-windows",
|
||||
"EXPORT_MD_CONSUMER": {
|
||||
"type": "BOOL",
|
||||
"value": "ON"
|
||||
@@ -222,6 +221,7 @@
|
||||
"APP_DATA_DIR": "${dollar}{APPDATA}/bts/DataStream",
|
||||
"APP_DATA_DIR_OVERRIDE": "${dollar}{DATA_STREAM_DATA_DIR}",
|
||||
"CONFIG_DEFAULTS_PATH": "config/profiles/default/*.conf;config/profiles/data_stream/*.conf",
|
||||
"DOTNET_BINARY_DIR": "${sourceDir}/build/$<CONFIG>/bin/net7.0;${sourceDir}/build/$<CONFIG>/bin/net7.0-windows;${sourceDir}/build/$<CONFIG>/bin/net8.0",
|
||||
"SHARED_MEMORY_PREFIX": "data_stream"
|
||||
}
|
||||
},
|
||||
@@ -258,7 +258,7 @@
|
||||
"CMAKE_CXX_FLAGS_RELEASE": "-fPIC -Wall -pedantic -Wextra -Werror -Wno-missing-braces -Wno-ignored-qualifiers --std=c++20 -fdiagnostics-color -fno-omit-frame-pointer -DNDEBUG -O3",
|
||||
"CMAKE_INSTALL_PREFIX": "${sourceDir}/install/${presetName}",
|
||||
"CONFIG_DEFAULTS_PATH": "config/profiles/default/*.conf;config/profiles/data_stream/*.conf",
|
||||
"DOTNET_BINARY_DIR": "${sourceDir}/build/$<CONFIG>/bin/net7.0",
|
||||
"DOTNET_BINARY_DIR": "${sourceDir}/build/$<CONFIG>/bin/net7.0;${sourceDir}/build/$<CONFIG>/bin/net8.0",
|
||||
"EXPORT_MD_CONSUMER": {
|
||||
"type": "BOOL",
|
||||
"value": "ON"
|
||||
|
||||
-1
Submodule docker/linux deleted from 56c61231f3.
Submodule docker/windows deleted from de7d40c09d.
@@ -8,7 +8,6 @@
|
||||
public int Integer;
|
||||
public double Real;
|
||||
public string Identifier;
|
||||
public DateTime DateTime;
|
||||
public ImmutableList<IArithmeticExpression> Arguments;
|
||||
public ImmutableList<Assignment> Assignments;
|
||||
public IArithmeticExpression Argument;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace BCG
|
||||
@@ -10,6 +12,7 @@ namespace BCG
|
||||
rk.SetValue (name, value.ToString ());
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
public static double getValue (RegistryKey rk, string name)
|
||||
{
|
||||
object v = rk.GetValue (name);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react/jsx-runtime',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
||||
settings: { react: { version: '18.3.1' } },
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.VisualStudio.JavaScript.Sdk/0.5.128-alpha">
|
||||
<PropertyGroup>
|
||||
<StartupCommand>npm run dev</StartupCommand>
|
||||
<JavaScriptTestRoot>src\</JavaScriptTestRoot>
|
||||
<JavaScriptTestFramework>Jest</JavaScriptTestFramework>
|
||||
<!-- Allows the build (or compile) script located on package.json to run on Build -->
|
||||
<ShouldRunBuildScript>true</ShouldRunBuildScript>
|
||||
<!-- Folder where production build objects will be placed -->
|
||||
<PublishAssetsDirectory>$(MSBuildProjectDirectory)\distill</PublishAssetsDirectory>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,8 @@
|
||||
# React + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/ico" href="/logo.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BTS Cloud Streams</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
</packageSources>
|
||||
<disabledPackageSources>
|
||||
<clear />
|
||||
</disabledPackageSources>
|
||||
</configuration>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "bts.datastreamwebapp.client",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/material": "^18.0.4",
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/styled": "^11.11.5",
|
||||
"@fontsource/noto-sans": "^5.0.22",
|
||||
"@material/data-table": "^15.0.0-canary.7f224ddd4.0",
|
||||
"@mui/icons-material": "^5.15.20",
|
||||
"@mui/material": "^5.15.20",
|
||||
"@mui/x-data-grid": "^7.7.1",
|
||||
"bulma": "^1.0.1",
|
||||
"react": "^18.3.1",
|
||||
"react-data-table-component": "^7.6.2",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-use-websocket": "^4.8.1",
|
||||
"rpc-websockets": "^7.11.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react": "^7.34.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-react-refresh": "^0.4.7",
|
||||
"vite": "^5.2.13"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,28 @@
|
||||
@use "@material/checkbox";
|
||||
@use "@material/icon-button";
|
||||
@use "@material/data-table/data-table";
|
||||
|
||||
h1#main-header {
|
||||
padding: 4px 8px 4px 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
td:hover {
|
||||
background-color: cadetblue;
|
||||
}
|
||||
|
||||
td#cell-decimal {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
td#cell-integer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
td#cell-string {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
td#cell-date-time {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { createTheme, ThemeProvider, CssBaseline } from '@mui/material';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import QueryControl from './QueryControl';
|
||||
import getTheme from './Theme';
|
||||
import '@fontsource/noto-sans';
|
||||
import './App.css';
|
||||
|
||||
const App = () => {
|
||||
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
|
||||
const theme = getTheme({ mode: prefersDarkMode ? 'dark' : 'light' });
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<h1 id='main-header' style={{
|
||||
color: theme.palette.text.primary,
|
||||
fontFamily: 'Noto Sans',
|
||||
}}>BTS Cloud Streams</h1>
|
||||
<QueryControl websocketUrl="ws://localhost:5000/ws" theme={theme} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,32 @@
|
||||
@use "@material/checkbox";
|
||||
@use "@material/icon-button";
|
||||
@use "@material/data-table/data-table";
|
||||
|
||||
th {
|
||||
font-size: large;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
td:hover {
|
||||
background-color: cadetblue;
|
||||
}
|
||||
|
||||
td#cell-decimal {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
td#cell-integer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
td#cell-string {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
td#cell-date-time {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { DataGrid } from '@mui/x-data-grid';
|
||||
|
||||
function getGridColDef(col) {
|
||||
return {
|
||||
field: col.name,
|
||||
headerName: col.name,
|
||||
};
|
||||
}
|
||||
|
||||
export default function DataTable({ columns, rows }) {
|
||||
return (
|
||||
<DataGrid
|
||||
columns={columns.map(getGridColDef)}
|
||||
rows={rows}
|
||||
sx={{
|
||||
borderColor: 'primary.main',
|
||||
backgroundColor: 'primary.dark',
|
||||
color: 'text.primary',
|
||||
'& .MuiDataGrid-columnHeader': {
|
||||
color: 'primary.main',
|
||||
fontWeight: 700,
|
||||
backgroundColor: 'primary.dark'
|
||||
},
|
||||
'& .MuiDataGrid-filler': {
|
||||
color: 'primary.main',
|
||||
fontWeight: 700,
|
||||
backgroundColor: 'primary.dark'
|
||||
},
|
||||
'& .MuiDataGrid-cell': {
|
||||
color: 'text.primary',
|
||||
},
|
||||
'& .MuiDataGrid-cell:hover': {
|
||||
color: 'primary.main',
|
||||
},
|
||||
'& .MuiDataGrid-row': {
|
||||
'--DataGrid-rowBorderColor': 'secondary.dark'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
DataTable.propTypes = {
|
||||
columns: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
rows: PropTypes.arrayOf(PropTypes.array).isRequired,
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import DataTable from './DataTable';
|
||||
import QueryInputForm from './QueryInputForm';
|
||||
import Container from '@mui/material/Container';
|
||||
import React from 'react';
|
||||
|
||||
var rpcReqIdCounter = 1;
|
||||
|
||||
function subscribeReq(query) {
|
||||
return {
|
||||
jsonrpc: "2.0",
|
||||
id: rpcReqIdCounter++,
|
||||
method: "subscribe",
|
||||
params: {
|
||||
subscription_id: "1",
|
||||
query: query
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function unsubscribeReq() {
|
||||
return {
|
||||
jsonrpc: "2.0",
|
||||
id: rpcReqIdCounter++,
|
||||
method: "unsubscribe",
|
||||
params: {
|
||||
subscription_id: "1",
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class QueryControl extends React.Component {
|
||||
constructor({ websocketUrl, theme }) {
|
||||
super(websocketUrl);
|
||||
console.log('Connecting to WebSocket:', websocketUrl);
|
||||
this.state = {
|
||||
rows: null,
|
||||
ws: new WebSocket(websocketUrl),
|
||||
columns: null,
|
||||
isRunning: false,
|
||||
theme: theme,
|
||||
};
|
||||
this.state.ws.onopen = () => {
|
||||
console.info('WebSocket open');
|
||||
};
|
||||
this.state.ws.onclose = () => {
|
||||
console.info('WebSocket closed');
|
||||
};
|
||||
this.state.ws.onmessage = e => {
|
||||
console.info(`Received message: ${e.data}`);
|
||||
const msg = JSON.parse(e.data);
|
||||
if (msg.id != null) {
|
||||
let newState = { ...this.state };
|
||||
newState.isRunning = msg.result.status == 'Subscribed';
|
||||
this.setState(newState);
|
||||
} else {
|
||||
let newState = { ...this.state };
|
||||
newState.rows = msg.params.all;
|
||||
console.info(`Received rows: ${newState.rows}`);
|
||||
newState.columns = msg.params.columns;
|
||||
this.setState(newState);
|
||||
}
|
||||
};
|
||||
|
||||
this.start = (query) => {
|
||||
this.state.ws.send(JSON.stringify(subscribeReq(query)));
|
||||
};
|
||||
|
||||
this.stop = () => {
|
||||
this.state.ws.send(JSON.stringify(unsubscribeReq()));
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.state.ws) {
|
||||
console.warn('Closing websocket because component is unmounting');
|
||||
this.state.ws.close();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
console.info('Rendering');
|
||||
return (
|
||||
<Container>
|
||||
<QueryInputForm isRunning={this.state.isRunning} onStart={this.start} onStop={this.stop} />
|
||||
{
|
||||
this.state.rows == null ? <p style={{ color: this.state.theme.palette.warning.main }}>No data received</p> :
|
||||
<DataTable columns={this.state.columns} rows={this.state.rows} />
|
||||
}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QueryControl.propTypes = {
|
||||
websocketUrl: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default QueryControl;
|
||||
@@ -0,0 +1,93 @@
|
||||
import { useRef } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||
import PropTypes from 'prop-types';
|
||||
import StopIcon from '@mui/icons-material/Stop';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
|
||||
const QueryInputForm = ({ isRunning, onStart, onStop }) => {
|
||||
let textInput = useRef(null);
|
||||
|
||||
const onClearClick = () => {
|
||||
textInput.current.value = '';
|
||||
};
|
||||
|
||||
const onRunClick = () => {
|
||||
onStart(textInput.current.value);
|
||||
};
|
||||
|
||||
const onStopClick = () => {
|
||||
onStop();
|
||||
};
|
||||
|
||||
const onKeyDown = (e) => {
|
||||
// Detect keyboard shortcuts.
|
||||
// Ctrl+Enter : Run
|
||||
if (e.ctrlKey && e.key === 'Enter') {
|
||||
onRunClick();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Paper elevation={3} sx={{ color: 'primary.main' }}>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={10}
|
||||
variant="outlined"
|
||||
onKeyDown={onKeyDown}
|
||||
inputRef={textInput}
|
||||
placeholder="Enter Query..."
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ p: 2, borderTop: 1 }}>
|
||||
<Grid container spacing={2} justifyContent="flex-start">
|
||||
<Grid item xs>
|
||||
<Tooltip title="Clear Query Text">
|
||||
<Button onClick={onClearClick} sx={{ padding: '11px', color: 'text.dark' }}>
|
||||
Clear
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Tooltip title="Run (Ctrl+Enter)">
|
||||
<Button onClick={onRunClick} sx={{
|
||||
color: `query_start_button.${isRunning ? 'active' : 'inactive'}.color`,
|
||||
bgcolor: `query_start_button.${isRunning ? 'active' : 'inactive'}.background_color`,
|
||||
padding: '11px'
|
||||
}}>
|
||||
<PlayArrowIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Tooltip title="Stop">
|
||||
<Button onClick={onStopClick} sx={{
|
||||
color: `query_stop_button.${isRunning ? 'inactive' : 'active'}.color`,
|
||||
bgcolor: `query_stop_button.${isRunning ? 'inactive' : 'active'}.background_color`,
|
||||
padding: '11px'
|
||||
}}>
|
||||
<StopIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
QueryInputForm.propTypes = {
|
||||
isRunning: PropTypes.bool.isRequired,
|
||||
onStart: PropTypes.func.isRequired,
|
||||
onStop: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default QueryInputForm;
|
||||
@@ -0,0 +1,88 @@
|
||||
import { createTheme } from '@mui/material/styles';
|
||||
import PropTypes from 'prop-types';
|
||||
import '@fontsource/noto-sans';
|
||||
import './App.css';
|
||||
|
||||
const getLightPalette = () => {
|
||||
return {
|
||||
mode: 'light',
|
||||
primary: {
|
||||
light: '#6573c3',
|
||||
main: '#3f51b5',
|
||||
dark: '#2c387e',
|
||||
contrastText: '#ffffff',
|
||||
},
|
||||
secondary: {
|
||||
light: '#f73378',
|
||||
main: '#f50057',
|
||||
dark: '#ab003c',
|
||||
contrastText: '#ffffff',
|
||||
},
|
||||
text: {
|
||||
primary: '#000000c4',
|
||||
secondary: '#00000086',
|
||||
disabled: '#0000005b',
|
||||
hint: '#0000005b',
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const getDarkPalette = () => {
|
||||
return {
|
||||
mode: 'dark',
|
||||
primary: {
|
||||
light: '#bcc5ff',
|
||||
main: '#33baf9',
|
||||
dark: '#202334',
|
||||
contrastText: '#78a2ac',
|
||||
},
|
||||
secondary: {
|
||||
light: '#9fd0b8',
|
||||
main: '#5eb289',
|
||||
dark: '#ffffff12',
|
||||
contrastText: '#c8d4f7',
|
||||
},
|
||||
text: {
|
||||
primary: '#eaf8f8',
|
||||
dark: '#eaf8f8a0'
|
||||
},
|
||||
query_start_button: {
|
||||
active: {
|
||||
color: '#33baf9',
|
||||
background_color: '#ffffff40',
|
||||
},
|
||||
inactive: {
|
||||
color: '#ffffff40',
|
||||
background_color: '#ffffff00',
|
||||
}
|
||||
},
|
||||
warning: {
|
||||
main: '#c4823f'
|
||||
},
|
||||
query_stop_button: {
|
||||
active: {
|
||||
color: '#ff4444a0',
|
||||
background_color: '#ffffff40',
|
||||
},
|
||||
inactive: {
|
||||
color: '#ff444450',
|
||||
background_color: '#ffffff00',
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const getTheme = ({ mode }) => {
|
||||
return createTheme({
|
||||
typography: {
|
||||
fontFamily: '"Helvetica"'
|
||||
},
|
||||
palette: mode == 'dark' ? getDarkPalette() : getLightPalette()
|
||||
});
|
||||
};
|
||||
|
||||
export default getTheme;
|
||||
|
||||
getTheme.propTypes = {
|
||||
mode: PropTypes.string.isRequired,
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,51 @@
|
||||
.dark-high-contrast {
|
||||
--md-sys-color-primary: rgb(251 250 255);
|
||||
--md-sys-color-surface-tint: rgb(165 200 255);
|
||||
--md-sys-color-on-primary: rgb(0 0 0);
|
||||
--md-sys-color-primary-container: rgb(173 204 255);
|
||||
--md-sys-color-on-primary-container: rgb(0 0 0);
|
||||
--md-sys-color-secondary: rgb(250 250 255);
|
||||
--md-sys-color-on-secondary: rgb(0 0 0);
|
||||
--md-sys-color-secondary-container: rgb(159 208 255);
|
||||
--md-sys-color-on-secondary-container: rgb(0 0 0);
|
||||
--md-sys-color-tertiary: rgb(255 249 251);
|
||||
--md-sys-color-on-tertiary: rgb(0 0 0);
|
||||
--md-sys-color-tertiary-container: rgb(222 193 231);
|
||||
--md-sys-color-on-tertiary-container: rgb(0 0 0);
|
||||
--md-sys-color-error: rgb(255 249 249);
|
||||
--md-sys-color-on-error: rgb(0 0 0);
|
||||
--md-sys-color-error-container: rgb(255 186 177);
|
||||
--md-sys-color-on-error-container: rgb(0 0 0);
|
||||
--md-sys-color-background: rgb(17 19 24);
|
||||
--md-sys-color-on-background: rgb(225 226 233);
|
||||
--md-sys-color-surface: rgb(17 19 24);
|
||||
--md-sys-color-on-surface: rgb(255 255 255);
|
||||
--md-sys-color-surface-variant: rgb(67 71 78);
|
||||
--md-sys-color-on-surface-variant: rgb(251 250 255);
|
||||
--md-sys-color-outline: rgb(200 202 212);
|
||||
--md-sys-color-outline-variant: rgb(200 202 212);
|
||||
--md-sys-color-shadow: rgb(0 0 0);
|
||||
--md-sys-color-scrim: rgb(0 0 0);
|
||||
--md-sys-color-inverse-surface: rgb(225 226 233);
|
||||
--md-sys-color-inverse-on-surface: rgb(0 0 0);
|
||||
--md-sys-color-inverse-primary: rgb(0 42 83);
|
||||
--md-sys-color-primary-fixed: rgb(219 231 255);
|
||||
--md-sys-color-on-primary-fixed: rgb(0 0 0);
|
||||
--md-sys-color-primary-fixed-dim: rgb(173 204 255);
|
||||
--md-sys-color-on-primary-fixed-variant: rgb(0 22 49);
|
||||
--md-sys-color-secondary-fixed: rgb(214 233 255);
|
||||
--md-sys-color-on-secondary-fixed: rgb(0 0 0);
|
||||
--md-sys-color-secondary-fixed-dim: rgb(159 208 255);
|
||||
--md-sys-color-on-secondary-fixed-variant: rgb(0 24 42);
|
||||
--md-sys-color-tertiary-fixed: rgb(249 222 255);
|
||||
--md-sys-color-on-tertiary-fixed: rgb(0 0 0);
|
||||
--md-sys-color-tertiary-fixed-dim: rgb(222 193 231);
|
||||
--md-sys-color-on-tertiary-fixed-variant: rgb(33 14 42);
|
||||
--md-sys-color-surface-dim: rgb(17 19 24);
|
||||
--md-sys-color-surface-bright: rgb(55 57 62);
|
||||
--md-sys-color-surface-container-lowest: rgb(12 14 19);
|
||||
--md-sys-color-surface-container-low: rgb(25 28 32);
|
||||
--md-sys-color-surface-container: rgb(29 32 36);
|
||||
--md-sys-color-surface-container-high: rgb(40 42 47);
|
||||
--md-sys-color-surface-container-highest: rgb(50 53 58);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
.dark-medium-contrast {
|
||||
--md-sys-color-primary: rgb(173 204 255);
|
||||
--md-sys-color-surface-tint: rgb(165 200 255);
|
||||
--md-sys-color-on-primary: rgb(0 22 49);
|
||||
--md-sys-color-primary-container: rgb(112 146 198);
|
||||
--md-sys-color-on-primary-container: rgb(0 0 0);
|
||||
--md-sys-color-secondary: rgb(159 208 255);
|
||||
--md-sys-color-on-secondary: rgb(0 24 42);
|
||||
--md-sys-color-secondary-container: rgb(100 149 193);
|
||||
--md-sys-color-on-secondary-container: rgb(0 0 0);
|
||||
--md-sys-color-tertiary: rgb(222 193 231);
|
||||
--md-sys-color-on-tertiary: rgb(33 14 42);
|
||||
--md-sys-color-tertiary-container: rgb(162 136 171);
|
||||
--md-sys-color-on-tertiary-container: rgb(0 0 0);
|
||||
--md-sys-color-error: rgb(255 186 177);
|
||||
--md-sys-color-on-error: rgb(55 0 1);
|
||||
--md-sys-color-error-container: rgb(255 84 73);
|
||||
--md-sys-color-on-error-container: rgb(0 0 0);
|
||||
--md-sys-color-background: rgb(17 19 24);
|
||||
--md-sys-color-on-background: rgb(225 226 233);
|
||||
--md-sys-color-surface: rgb(17 19 24);
|
||||
--md-sys-color-on-surface: rgb(251 250 255);
|
||||
--md-sys-color-surface-variant: rgb(67 71 78);
|
||||
--md-sys-color-on-surface-variant: rgb(200 202 212);
|
||||
--md-sys-color-outline: rgb(160 163 171);
|
||||
--md-sys-color-outline-variant: rgb(128 131 139);
|
||||
--md-sys-color-shadow: rgb(0 0 0);
|
||||
--md-sys-color-scrim: rgb(0 0 0);
|
||||
--md-sys-color-inverse-surface: rgb(225 226 233);
|
||||
--md-sys-color-inverse-on-surface: rgb(40 42 47);
|
||||
--md-sys-color-inverse-primary: rgb(35 73 120);
|
||||
--md-sys-color-primary-fixed: rgb(212 227 255);
|
||||
--md-sys-color-on-primary-fixed: rgb(0 17 40);
|
||||
--md-sys-color-primary-fixed-dim: rgb(165 200 255);
|
||||
--md-sys-color-on-primary-fixed-variant: rgb(10 55 100);
|
||||
--md-sys-color-secondary-fixed: rgb(206 229 255);
|
||||
--md-sys-color-on-secondary-fixed: rgb(0 18 34);
|
||||
--md-sys-color-secondary-fixed-dim: rgb(154 203 250);
|
||||
--md-sys-color-on-secondary-fixed-variant: rgb(0 57 91);
|
||||
--md-sys-color-tertiary-fixed: rgb(247 216 255);
|
||||
--md-sys-color-on-tertiary-fixed: rgb(28 9 37);
|
||||
--md-sys-color-tertiary-fixed-dim: rgb(218 189 226);
|
||||
--md-sys-color-on-tertiary-fixed-variant: rgb(67 46 76);
|
||||
--md-sys-color-surface-dim: rgb(17 19 24);
|
||||
--md-sys-color-surface-bright: rgb(55 57 62);
|
||||
--md-sys-color-surface-container-lowest: rgb(12 14 19);
|
||||
--md-sys-color-surface-container-low: rgb(25 28 32);
|
||||
--md-sys-color-surface-container: rgb(29 32 36);
|
||||
--md-sys-color-surface-container-high: rgb(40 42 47);
|
||||
--md-sys-color-surface-container-highest: rgb(50 53 58);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
.dark {
|
||||
--md-sys-color-primary: rgb(165 200 255);
|
||||
--md-sys-color-surface-tint: rgb(165 200 255);
|
||||
--md-sys-color-on-primary: rgb(0 49 94);
|
||||
--md-sys-color-primary-container: rgb(34 72 118);
|
||||
--md-sys-color-on-primary-container: rgb(212 227 255);
|
||||
--md-sys-color-secondary: rgb(154 203 250);
|
||||
--md-sys-color-on-secondary: rgb(0 51 83);
|
||||
--md-sys-color-secondary-container: rgb(12 74 114);
|
||||
--md-sys-color-on-secondary-container: rgb(206 229 255);
|
||||
--md-sys-color-tertiary: rgb(218 189 226);
|
||||
--md-sys-color-on-tertiary: rgb(61 41 70);
|
||||
--md-sys-color-tertiary-container: rgb(85 63 93);
|
||||
--md-sys-color-on-tertiary-container: rgb(247 216 255);
|
||||
--md-sys-color-error: rgb(255 180 171);
|
||||
--md-sys-color-on-error: rgb(105 0 5);
|
||||
--md-sys-color-error-container: rgb(147 0 10);
|
||||
--md-sys-color-on-error-container: rgb(255 218 214);
|
||||
--md-sys-color-background: rgb(17 19 24);
|
||||
--md-sys-color-on-background: rgb(225 226 233);
|
||||
--md-sys-color-surface: rgb(17 19 24);
|
||||
--md-sys-color-on-surface: rgb(225 226 233);
|
||||
--md-sys-color-surface-variant: rgb(67 71 78);
|
||||
--md-sys-color-on-surface-variant: rgb(195 198 207);
|
||||
--md-sys-color-outline: rgb(141 145 153);
|
||||
--md-sys-color-outline-variant: rgb(67 71 78);
|
||||
--md-sys-color-shadow: rgb(0 0 0);
|
||||
--md-sys-color-scrim: rgb(0 0 0);
|
||||
--md-sys-color-inverse-surface: rgb(225 226 233);
|
||||
--md-sys-color-inverse-on-surface: rgb(46 48 53);
|
||||
--md-sys-color-inverse-primary: rgb(60 96 144);
|
||||
--md-sys-color-primary-fixed: rgb(212 227 255);
|
||||
--md-sys-color-on-primary-fixed: rgb(0 28 58);
|
||||
--md-sys-color-primary-fixed-dim: rgb(165 200 255);
|
||||
--md-sys-color-on-primary-fixed-variant: rgb(34 72 118);
|
||||
--md-sys-color-secondary-fixed: rgb(206 229 255);
|
||||
--md-sys-color-on-secondary-fixed: rgb(0 29 50);
|
||||
--md-sys-color-secondary-fixed-dim: rgb(154 203 250);
|
||||
--md-sys-color-on-secondary-fixed-variant: rgb(12 74 114);
|
||||
--md-sys-color-tertiary-fixed: rgb(247 216 255);
|
||||
--md-sys-color-on-tertiary-fixed: rgb(39 20 48);
|
||||
--md-sys-color-tertiary-fixed-dim: rgb(218 189 226);
|
||||
--md-sys-color-on-tertiary-fixed-variant: rgb(85 63 93);
|
||||
--md-sys-color-surface-dim: rgb(17 19 24);
|
||||
--md-sys-color-surface-bright: rgb(55 57 62);
|
||||
--md-sys-color-surface-container-lowest: rgb(12 14 19);
|
||||
--md-sys-color-surface-container-low: rgb(25 28 32);
|
||||
--md-sys-color-surface-container: rgb(29 32 36);
|
||||
--md-sys-color-surface-container-high: rgb(40 42 47);
|
||||
--md-sys-color-surface-container-highest: rgb(50 53 58);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
.light-high-contrast {
|
||||
--md-sys-color-primary: rgb(0 34 70);
|
||||
--md-sys-color-surface-tint: rgb(60 96 144);
|
||||
--md-sys-color-on-primary: rgb(255 255 255);
|
||||
--md-sys-color-primary-container: rgb(29 68 114);
|
||||
--md-sys-color-on-primary-container: rgb(255 255 255);
|
||||
--md-sys-color-secondary: rgb(0 36 61);
|
||||
--md-sys-color-on-secondary: rgb(255 255 255);
|
||||
--md-sys-color-secondary-container: rgb(3 70 110);
|
||||
--md-sys-color-on-secondary-container: rgb(255 255 255);
|
||||
--md-sys-color-tertiary: rgb(46 26 55);
|
||||
--md-sys-color-on-tertiary: rgb(255 255 255);
|
||||
--md-sys-color-tertiary-container: rgb(81 59 89);
|
||||
--md-sys-color-on-tertiary-container: rgb(255 255 255);
|
||||
--md-sys-color-error: rgb(78 0 2);
|
||||
--md-sys-color-on-error: rgb(255 255 255);
|
||||
--md-sys-color-error-container: rgb(140 0 9);
|
||||
--md-sys-color-on-error-container: rgb(255 255 255);
|
||||
--md-sys-color-background: rgb(249 249 255);
|
||||
--md-sys-color-on-background: rgb(25 28 32);
|
||||
--md-sys-color-surface: rgb(249 249 255);
|
||||
--md-sys-color-on-surface: rgb(0 0 0);
|
||||
--md-sys-color-surface-variant: rgb(224 226 236);
|
||||
--md-sys-color-on-surface-variant: rgb(32 36 43);
|
||||
--md-sys-color-outline: rgb(63 67 74);
|
||||
--md-sys-color-outline-variant: rgb(63 67 74);
|
||||
--md-sys-color-shadow: rgb(0 0 0);
|
||||
--md-sys-color-scrim: rgb(0 0 0);
|
||||
--md-sys-color-inverse-surface: rgb(46 48 53);
|
||||
--md-sys-color-inverse-on-surface: rgb(255 255 255);
|
||||
--md-sys-color-inverse-primary: rgb(228 236 255);
|
||||
--md-sys-color-primary-fixed: rgb(29 68 114);
|
||||
--md-sys-color-on-primary-fixed: rgb(255 255 255);
|
||||
--md-sys-color-primary-fixed-dim: rgb(0 45 88);
|
||||
--md-sys-color-on-primary-fixed-variant: rgb(255 255 255);
|
||||
--md-sys-color-secondary-fixed: rgb(3 70 110);
|
||||
--md-sys-color-on-secondary-fixed: rgb(255 255 255);
|
||||
--md-sys-color-secondary-fixed-dim: rgb(0 47 77);
|
||||
--md-sys-color-on-secondary-fixed-variant: rgb(255 255 255);
|
||||
--md-sys-color-tertiary-fixed: rgb(81 59 89);
|
||||
--md-sys-color-on-tertiary-fixed: rgb(255 255 255);
|
||||
--md-sys-color-tertiary-fixed-dim: rgb(57 37 66);
|
||||
--md-sys-color-on-tertiary-fixed-variant: rgb(255 255 255);
|
||||
--md-sys-color-surface-dim: rgb(217 218 224);
|
||||
--md-sys-color-surface-bright: rgb(249 249 255);
|
||||
--md-sys-color-surface-container-lowest: rgb(255 255 255);
|
||||
--md-sys-color-surface-container-low: rgb(243 243 250);
|
||||
--md-sys-color-surface-container: rgb(237 237 244);
|
||||
--md-sys-color-surface-container-high: rgb(231 232 238);
|
||||
--md-sys-color-surface-container-highest: rgb(225 226 233);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
.light-medium-contrast {
|
||||
--md-sys-color-primary: rgb(29 68 114);
|
||||
--md-sys-color-surface-tint: rgb(60 96 144);
|
||||
--md-sys-color-on-primary: rgb(255 255 255);
|
||||
--md-sys-color-primary-container: rgb(83 118 167);
|
||||
--md-sys-color-on-primary-container: rgb(255 255 255);
|
||||
--md-sys-color-secondary: rgb(3 70 110);
|
||||
--md-sys-color-on-secondary: rgb(255 255 255);
|
||||
--md-sys-color-secondary-container: rgb(71 121 163);
|
||||
--md-sys-color-on-secondary-container: rgb(255 255 255);
|
||||
--md-sys-color-tertiary: rgb(81 59 89);
|
||||
--md-sys-color-on-tertiary: rgb(255 255 255);
|
||||
--md-sys-color-tertiary-container: rgb(133 108 141);
|
||||
--md-sys-color-on-tertiary-container: rgb(255 255 255);
|
||||
--md-sys-color-error: rgb(140 0 9);
|
||||
--md-sys-color-on-error: rgb(255 255 255);
|
||||
--md-sys-color-error-container: rgb(218 52 46);
|
||||
--md-sys-color-on-error-container: rgb(255 255 255);
|
||||
--md-sys-color-background: rgb(249 249 255);
|
||||
--md-sys-color-on-background: rgb(25 28 32);
|
||||
--md-sys-color-surface: rgb(249 249 255);
|
||||
--md-sys-color-on-surface: rgb(25 28 32);
|
||||
--md-sys-color-surface-variant: rgb(224 226 236);
|
||||
--md-sys-color-on-surface-variant: rgb(63 67 74);
|
||||
--md-sys-color-outline: rgb(91 95 103);
|
||||
--md-sys-color-outline-variant: rgb(119 123 131);
|
||||
--md-sys-color-shadow: rgb(0 0 0);
|
||||
--md-sys-color-scrim: rgb(0 0 0);
|
||||
--md-sys-color-inverse-surface: rgb(46 48 53);
|
||||
--md-sys-color-inverse-on-surface: rgb(240 240 247);
|
||||
--md-sys-color-inverse-primary: rgb(165 200 255);
|
||||
--md-sys-color-primary-fixed: rgb(83 118 167);
|
||||
--md-sys-color-on-primary-fixed: rgb(255 255 255);
|
||||
--md-sys-color-primary-fixed-dim: rgb(58 93 141);
|
||||
--md-sys-color-on-primary-fixed-variant: rgb(255 255 255);
|
||||
--md-sys-color-secondary-fixed: rgb(71 121 163);
|
||||
--md-sys-color-on-secondary-fixed: rgb(255 255 255);
|
||||
--md-sys-color-secondary-fixed-dim: rgb(43 96 137);
|
||||
--md-sys-color-on-secondary-fixed-variant: rgb(255 255 255);
|
||||
--md-sys-color-tertiary-fixed: rgb(133 108 141);
|
||||
--md-sys-color-on-tertiary-fixed: rgb(255 255 255);
|
||||
--md-sys-color-tertiary-fixed-dim: rgb(107 84 116);
|
||||
--md-sys-color-on-tertiary-fixed-variant: rgb(255 255 255);
|
||||
--md-sys-color-surface-dim: rgb(217 218 224);
|
||||
--md-sys-color-surface-bright: rgb(249 249 255);
|
||||
--md-sys-color-surface-container-lowest: rgb(255 255 255);
|
||||
--md-sys-color-surface-container-low: rgb(243 243 250);
|
||||
--md-sys-color-surface-container: rgb(237 237 244);
|
||||
--md-sys-color-surface-container-high: rgb(231 232 238);
|
||||
--md-sys-color-surface-container-highest: rgb(225 226 233);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
.light {
|
||||
--md-sys-color-primary: rgb(60 96 144);
|
||||
--md-sys-color-surface-tint: rgb(60 96 144);
|
||||
--md-sys-color-on-primary: rgb(255 255 255);
|
||||
--md-sys-color-primary-container: rgb(212 227 255);
|
||||
--md-sys-color-on-primary-container: rgb(0 28 58);
|
||||
--md-sys-color-secondary: rgb(46 98 140);
|
||||
--md-sys-color-on-secondary: rgb(255 255 255);
|
||||
--md-sys-color-secondary-container: rgb(206 229 255);
|
||||
--md-sys-color-on-secondary-container: rgb(0 29 50);
|
||||
--md-sys-color-tertiary: rgb(110 86 118);
|
||||
--md-sys-color-on-tertiary: rgb(255 255 255);
|
||||
--md-sys-color-tertiary-container: rgb(247 216 255);
|
||||
--md-sys-color-on-tertiary-container: rgb(39 20 48);
|
||||
--md-sys-color-error: rgb(186 26 26);
|
||||
--md-sys-color-on-error: rgb(255 255 255);
|
||||
--md-sys-color-error-container: rgb(255 218 214);
|
||||
--md-sys-color-on-error-container: rgb(65 0 2);
|
||||
--md-sys-color-background: rgb(249 249 255);
|
||||
--md-sys-color-on-background: rgb(25 28 32);
|
||||
--md-sys-color-surface: rgb(249 249 255);
|
||||
--md-sys-color-on-surface: rgb(25 28 32);
|
||||
--md-sys-color-surface-variant: rgb(224 226 236);
|
||||
--md-sys-color-on-surface-variant: rgb(67 71 78);
|
||||
--md-sys-color-outline: rgb(116 119 127);
|
||||
--md-sys-color-outline-variant: rgb(195 198 207);
|
||||
--md-sys-color-shadow: rgb(0 0 0);
|
||||
--md-sys-color-scrim: rgb(0 0 0);
|
||||
--md-sys-color-inverse-surface: rgb(46 48 53);
|
||||
--md-sys-color-inverse-on-surface: rgb(240 240 247);
|
||||
--md-sys-color-inverse-primary: rgb(165 200 255);
|
||||
--md-sys-color-primary-fixed: rgb(212 227 255);
|
||||
--md-sys-color-on-primary-fixed: rgb(0 28 58);
|
||||
--md-sys-color-primary-fixed-dim: rgb(165 200 255);
|
||||
--md-sys-color-on-primary-fixed-variant: rgb(34 72 118);
|
||||
--md-sys-color-secondary-fixed: rgb(206 229 255);
|
||||
--md-sys-color-on-secondary-fixed: rgb(0 29 50);
|
||||
--md-sys-color-secondary-fixed-dim: rgb(154 203 250);
|
||||
--md-sys-color-on-secondary-fixed-variant: rgb(12 74 114);
|
||||
--md-sys-color-tertiary-fixed: rgb(247 216 255);
|
||||
--md-sys-color-on-tertiary-fixed: rgb(39 20 48);
|
||||
--md-sys-color-tertiary-fixed-dim: rgb(218 189 226);
|
||||
--md-sys-color-on-tertiary-fixed-variant: rgb(85 63 93);
|
||||
--md-sys-color-surface-dim: rgb(217 218 224);
|
||||
--md-sys-color-surface-bright: rgb(249 249 255);
|
||||
--md-sys-color-surface-container-lowest: rgb(255 255 255);
|
||||
--md-sys-color-surface-container-low: rgb(243 243 250);
|
||||
--md-sys-color-surface-container: rgb(237 237 244);
|
||||
--md-sys-color-surface-container-high: rgb(231 232 238);
|
||||
--md-sys-color-surface-container-highest: rgb(225 226 233);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
button#query-button {
|
||||
height: 5em;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
button#play-button {
|
||||
padding: '10px 20px';
|
||||
font-size: '16px';
|
||||
cursor: 'pointer';
|
||||
background-color: '#28a745';
|
||||
color: '#fff';
|
||||
border: 'none';
|
||||
border-radius: '5px';
|
||||
};
|
||||
|
||||
button#pause-button {
|
||||
background-color: #ffc107;
|
||||
}
|
||||
|
||||
button#stop-button {
|
||||
background-color: #dc3545;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
p#footer {
|
||||
float: none;
|
||||
clear: both;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
table#query-output {
|
||||
float: none;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
textarea#query-input {
|
||||
height: 5em;
|
||||
width: 50em;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
)
|
||||
@@ -0,0 +1,69 @@
|
||||
import { fileURLToPath, URL } from 'node:url';
|
||||
import { defineConfig } from 'vite';
|
||||
import plugin from '@vitejs/plugin-react';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import child_process from 'child_process';
|
||||
import process, { env } from 'process';
|
||||
|
||||
const baseFolder =
|
||||
process.env.APPDATA !== undefined && process.env.APPDATA !== ''
|
||||
? `${process.env.APPDATA}/ASP.NET/https`
|
||||
: `${process.env.HOME}/.aspnet/https`;
|
||||
|
||||
const certificateName = "bts.datastreamwebapp.client";
|
||||
const certFilePath = path.join(baseFolder, `${certificateName}.pem`);
|
||||
const keyFilePath = path.join(baseFolder, `${certificateName}.key`);
|
||||
|
||||
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
|
||||
if (0 !== child_process.spawnSync('dotnet', [
|
||||
'dev-certs',
|
||||
'https',
|
||||
'--export-path',
|
||||
certFilePath,
|
||||
'--format',
|
||||
'Pem',
|
||||
'--no-password',
|
||||
], { stdio: 'inherit', }).status) {
|
||||
throw new Error("Could not create certificate.");
|
||||
}
|
||||
}
|
||||
|
||||
const target = env.ASPNETCORE_HTTPS_PORT
|
||||
? `https://localhost:${env.ASPNETCORE_HTTPS_PORT}`
|
||||
: env.ASPNETCORE_URLS
|
||||
? env.ASPNETCORE_URLS.split(';')[0]
|
||||
: 'https://localhost:5001';
|
||||
|
||||
export default defineConfig(({ $$command, mode, $$isSsrBuild, $$isPreview }) => {
|
||||
return {
|
||||
plugins: [plugin()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
build: {
|
||||
minify: false,
|
||||
sourcemap: true,
|
||||
outDir: `../../../build/Bts.DataStreamWebApp.Client/${mode}/dist`
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'^/api': {
|
||||
target,
|
||||
secure: false
|
||||
},
|
||||
'^/ws': {
|
||||
target,
|
||||
secure: false
|
||||
}
|
||||
},
|
||||
port: 5173,
|
||||
https: {
|
||||
key: fs.readFileSync(keyFilePath),
|
||||
cert: fs.readFileSync(certFilePath),
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
<SpaRoot>..\Bts.DataStreamWebApp.Client</SpaRoot>
|
||||
<SpaProxyLaunchCommand>npm run dev</SpaProxyLaunchCommand>
|
||||
<SpaProxyServerUrl>https://localhost:5173</SpaProxyServerUrl>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaProxy" Version="7.0.20" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="7.0.20" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Bts.DataStreamWebApp.Client\Bts.DataStreamWebApp.Client.esproj">
|
||||
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\DataStream\DataStream.csproj" />
|
||||
<ProjectReference Include="..\SharedStreams\SharedStreams.csproj" />
|
||||
<ProjectReference Include="..\..\client\spark_api\spark_api.csproj" />
|
||||
<ProjectReference Include="..\..\client\spark_core\spark_core.csproj" />
|
||||
<ProjectReference Include="..\..\client\spark_websocket\spark_websocket.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- <ItemGroup>
|
||||
<Content Update="wwwroot\alt-index.html" CopyToPublishDirectory="Never" />
|
||||
</ItemGroup> -->
|
||||
</Project>
|
||||
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"MasterWorker": {
|
||||
"Interval": "00:00:00.1"
|
||||
},
|
||||
"ViewStreamDatabase": {
|
||||
"Interval": "00:00:00.1"
|
||||
},
|
||||
"Sparkd": {
|
||||
"Config": {
|
||||
"ConfigServer": "http://localhost:8000/config"
|
||||
},
|
||||
"Credentials": {
|
||||
"User": "bts",
|
||||
"Password": ""
|
||||
},
|
||||
"EnableAdmin": true,
|
||||
"EnableOrders": true,
|
||||
"EnablePosition": true
|
||||
},
|
||||
"PubSub": {
|
||||
"MillisecondsBetweenSubInfoUpdates": 1000,
|
||||
"MillisecondsBetweenTimeSliceUpdates": 5000,
|
||||
"MillisecondsBetweenNoBufferSpaceRetries": 100,
|
||||
"RetriesOnNoBufferSpace": 1,
|
||||
"DoItAllSecondsBetweenLogs": 0,
|
||||
"HeartbeatInterval": 15000,
|
||||
"MissingHeartbeatsBeforeGc": 50,
|
||||
"UseManagedSockets": true,
|
||||
"StateUpdatePeriod": 5000,
|
||||
"StatisticsUpdatePeriod": 5000,
|
||||
"DisposedConnectionCheckPeriod": 1000,
|
||||
"MessageProcessingWarningThreshold": 1000,
|
||||
"ServerPort": 12345,
|
||||
"Routing": {
|
||||
"PublisherRoutes": [
|
||||
{
|
||||
"Name": "default",
|
||||
"ServerAddress": "bts-cloud://127.0.0.1:12345",
|
||||
"TagRegExp": ".*"
|
||||
}
|
||||
],
|
||||
"SubscriberRoutes": [
|
||||
{
|
||||
"Name": "default",
|
||||
"ServerAddress": "bts-cloud://127.0.0.1:12345",
|
||||
"TagRegExp": ".*"
|
||||
}
|
||||
]
|
||||
},
|
||||
"MaxPacketSize": 500000000,
|
||||
"BlueCommMustDie": true,
|
||||
"CrushableTags": [],
|
||||
"TagTypeToAssemblyMap": {
|
||||
"Entries": [
|
||||
{
|
||||
"HandlerClass": "BCG.PubSubServerTagHandler",
|
||||
"Assembly": "DataStreamCore.dll"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ServerQueue": "PubSubQueue",
|
||||
"PortRange": {
|
||||
"First": 30400,
|
||||
"Last": 31700
|
||||
},
|
||||
"BindToRandomPort": false,
|
||||
"DebugTags": "^NOTAGS$",
|
||||
"LatencyReportThresholdMs": 1500,
|
||||
"UseSubstations": false,
|
||||
"UseDynamicClients": false,
|
||||
"IsSubstation": false,
|
||||
"InternallyPublishSubInfo": true,
|
||||
"UseManagedClientsInNativeCode": false,
|
||||
"CompressedTags": "^NOTAGS$",
|
||||
"TagsToReportCompressionRatio": "^NOTAGS$",
|
||||
"DisableMsmq": false,
|
||||
"UsePrepublishedServerState": false,
|
||||
"IpTranslation": null,
|
||||
"GcActivationThreshold": 0,
|
||||
"WaitPeriodAfterGc": 1000,
|
||||
"TagHandlerFactory": {
|
||||
"Handlers": [
|
||||
{
|
||||
"TagType": "DataStream",
|
||||
"Assembly": "DataStreamCore",
|
||||
"Class": "bts.PubsubServerStreamTagHandler"
|
||||
},
|
||||
{
|
||||
"TagType": "System_Sub",
|
||||
"Assembly": "PubSub",
|
||||
"Class": "BCG.SystemSubscriptionMapPublisher"
|
||||
},
|
||||
{
|
||||
"TagType": "System_Stats",
|
||||
"Assembly": "PubSub",
|
||||
"Class": "BCG.SystemStatsPublisher"
|
||||
},
|
||||
{
|
||||
"TagType": ".*",
|
||||
"Assembly": "DataStreamCore",
|
||||
"Class": "bts.PubsubServerGenericDataHandler"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"content_root_path": "build/Bts.DataStreamWebApp.Client/production",
|
||||
"web_root_path": "dist"
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
namespace Bts.DataStreamWebApp.Server.Controllers;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
[Route("/ws")]
|
||||
public class DataStreamSubscriptionController : ControllerBase {
|
||||
readonly ILogger logger;
|
||||
|
||||
public DataStreamSubscriptionController(ILogger<DataStreamSubscriptionController> logger) {
|
||||
this.logger = logger;
|
||||
logger.LogInformation("Creating {ControllerType}", GetType().Name);
|
||||
}
|
||||
|
||||
[Route("/ws")]
|
||||
public async Task Get(CancellationToken cancellationToken) {
|
||||
if (!HttpContext.WebSockets.IsWebSocketRequest) {
|
||||
logger.LogError("Expected websocket request on path '{}'", HttpContext.Request.Path);
|
||||
return;
|
||||
}
|
||||
logger.LogInformation("Establishing websocket connection...");
|
||||
try {
|
||||
using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
logger.LogInformation("Websocket connection established.");
|
||||
using var server = new DataStreamWebSocketServer(webSocket, logger);
|
||||
await server.Run(cancellationToken);
|
||||
} catch (Exception ex) {
|
||||
logger.LogError("DataStreamSubscriptionController.Run threw exception: {Exception}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
namespace Bts.DataStreamWebApp.Server.Controllers;
|
||||
|
||||
using BCG;
|
||||
|
||||
using bts.websocket_core.json_rpc;
|
||||
|
||||
using Bts.DataStreamCore;
|
||||
using Bts.DataStreamWebApp.Server.Messages;
|
||||
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
class DataStreamSubscription : IDisposable, IHostedService {
|
||||
public readonly ViewStream ViewStream;
|
||||
public DataPacket? LastFired;
|
||||
|
||||
readonly ILogger logger;
|
||||
readonly WebSocket webSocket;
|
||||
|
||||
public DataStreamSubscription(string id, JsonRpcId requestId, ViewStream viewStream, WebSocket webSocket, ILogger logger) {
|
||||
ViewStream = viewStream;
|
||||
RequestId = requestId;
|
||||
Id = id;
|
||||
this.webSocket = webSocket;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public string Id { get; }
|
||||
public JsonRpcId RequestId { get; }
|
||||
public SubscriptionStatus Status { get; private set; }
|
||||
|
||||
public void Start() {
|
||||
if (Status == SubscriptionStatus.Subscribed) {
|
||||
logger.LogWarning("Ignoring redundant call to 'Start'. Id = {Id} Status = {Status}", Id, Status);
|
||||
return;
|
||||
}
|
||||
logger.LogInformation("Subscribing to ViewStream. SubscriptionId = {SubscriptionId}", Id);
|
||||
Status = SubscriptionStatus.Subscribed;
|
||||
ViewStream.onChange += onChange;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken) {
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
Start();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Clear() {
|
||||
if (LastFired is null) {
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
logger.LogInformation("Clearing subscription (firing DataPacket to delete all rows). Id = {Id} Status = {Status}", Id, Status);
|
||||
return sendDataPacket("clear", new(new BlueTable(LastFired.schema), new BlueTable(LastFired.schema), LastFired.newRows));
|
||||
}
|
||||
|
||||
public void Stop() {
|
||||
if (Status == SubscriptionStatus.Unsubscribed) {
|
||||
logger.LogWarning("Ignoring redundant call to 'Stop'. Id = {Id} Status = {Status}", Id, Status);
|
||||
return;
|
||||
}
|
||||
logger.LogInformation("Unsubscribing from ViewStream. SubscriptionId = {SubscriptionId}", Id);
|
||||
Status = SubscriptionStatus.Unsubscribed;
|
||||
ViewStream.onChange -= onChange;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) {
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
Stop();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose() =>
|
||||
Stop();
|
||||
|
||||
async void onChange(object newValue) {
|
||||
if (newValue is not DataPacket dp) {
|
||||
logger.LogError("Expected DataPacket in stream callback (actually received {}).", newValue?.GetType());
|
||||
return;
|
||||
}
|
||||
if (webSocket.State != WebSocketState.Open) {
|
||||
logger.LogError(
|
||||
"Unsubscribing from stream early because WebSocket state = {WebSocketState}", webSocket.State);
|
||||
Dispose();
|
||||
return;
|
||||
}
|
||||
await sendDataPacket("subscribe", dp);
|
||||
}
|
||||
|
||||
async Task sendDataPacket(string method, DataPacket dp) {
|
||||
var serializerOpts = new JsonSerializerOptions {
|
||||
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals
|
||||
};
|
||||
serializerOpts.Converters.Add(new DataPacketJsonConverter() {
|
||||
IncludeCurrent = true,
|
||||
UseObjectRows = true,
|
||||
});
|
||||
var res = JsonRpcRequest.CreateNotification(method, dp);
|
||||
var str = serialize(res, serializerOpts);
|
||||
if (str is null) {
|
||||
logger.LogError("Unsubscribing from stream early because we failed to serialize an outgoing DataPacket");
|
||||
Dispose();
|
||||
return;
|
||||
}
|
||||
logger.LogInformation(
|
||||
"Sending DataPacket. NumRows = {NumRows} NumCols = {NumCols} MessageLength = {MessageLength}",
|
||||
dp.newRows.Count,
|
||||
dp.schema.columnNames.Length,
|
||||
str.Length);
|
||||
var bytes = Encoding.UTF8.GetBytes(str);
|
||||
await webSocket.SendAsync(bytes, WebSocketMessageType.Text, true, CancellationToken.None);
|
||||
LastFired = dp;
|
||||
}
|
||||
|
||||
string? serialize<T>(T message, JsonSerializerOptions? opts) {
|
||||
opts ??= JsonSerializerOptions.Default;
|
||||
try {
|
||||
return JsonSerializer.Serialize(message, opts);
|
||||
} catch (Exception ex) {
|
||||
logger.LogError("Failed to serialize message of type {MessageType}: {Exception}", typeof(T), ex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
namespace Bts.DataStreamWebApp.Server;
|
||||
|
||||
using Bts.PubSub;
|
||||
|
||||
using CommandLine;
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
class DataStreamWebAppServerConfig {
|
||||
/// <summary>
|
||||
/// The login user for sparkd
|
||||
/// </summary>
|
||||
[JsonPropertyName("user")]
|
||||
[Option("user", HelpText = "The sparkd login user")]
|
||||
public string? User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The login password for sparkd
|
||||
/// </summary>
|
||||
[JsonPropertyName("password")]
|
||||
[Option("pass", HelpText = "The sparkd login password")]
|
||||
public string? Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The config server url for sparkd
|
||||
/// </summary>
|
||||
[JsonPropertyName("sparkd")]
|
||||
[Option("sparkd", HelpText = "The sparkd config server")]
|
||||
public string? Sparkd { get; set; }
|
||||
|
||||
[JsonPropertyName("pubsub")]
|
||||
public PubSubConfig? PubSub { get; set; }
|
||||
|
||||
[Option("content-root-path")]
|
||||
[JsonPropertyName("content_root_path")]
|
||||
public string? ContentRootPath { get; set; }
|
||||
|
||||
[Option("web-root-path")]
|
||||
[JsonPropertyName("web_root_path")]
|
||||
public string? WebRootPath { get; set; }
|
||||
|
||||
[Option("environment-name")]
|
||||
[JsonPropertyName("environment_name")]
|
||||
public string? EnvironmentName { get; set; }
|
||||
|
||||
public DirectoryBrowserOptions? DirectoryBrowser { get; set; }
|
||||
|
||||
[Option(
|
||||
"config",
|
||||
MetaValue = "PATH",
|
||||
HelpText = "Path to json file representing these options (DataStreamPublisherConfig)")]
|
||||
[JsonIgnore]
|
||||
public string? ConfigFile { get; set; }
|
||||
|
||||
public static DataStreamWebAppServerConfig Default() =>
|
||||
new() {
|
||||
User = "bts",
|
||||
PubSub = PubSubConfig.Default(),
|
||||
};
|
||||
|
||||
public void MergeWith(DataStreamWebAppServerConfig other) {
|
||||
User ??= other.User;
|
||||
Password ??= other.Password;
|
||||
PubSub ??= other.PubSub;
|
||||
Sparkd ??= other.Sparkd;
|
||||
ContentRootPath ??= other.ContentRootPath;
|
||||
WebRootPath ??= other.WebRootPath;
|
||||
ConfigFile ??= other.ConfigFile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges options with those defined in `ConfigFile` (does not overwrite
|
||||
/// non-null existing values).
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// JsonSerializer.Deserialize returns null
|
||||
/// </exception>
|
||||
public void MergeWithConfigFile() {
|
||||
if (ConfigFile is null) {
|
||||
return;
|
||||
}
|
||||
var cfgString = File.ReadAllText(ConfigFile);
|
||||
var parsedCfg = JsonSerializer.Deserialize<DataStreamWebAppServerConfig>(cfgString) ??
|
||||
throw new InvalidOperationException($"Failed to parse json from {ConfigFile}");
|
||||
MergeWith(parsedCfg);
|
||||
ConfigFile = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
namespace Bts.DataStreamWebApp.Server.Controllers;
|
||||
|
||||
using bts.websocket_core.json_rpc;
|
||||
|
||||
using Bts.DataStreamWebApp.Server.Messages;
|
||||
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
public class DataStreamWebSocketServer : IDisposable, IAsyncDisposable {
|
||||
readonly ILogger logger;
|
||||
readonly WebSocket webSocket;
|
||||
readonly Dictionary<string, DataStreamSubscription> subscriptions = new();
|
||||
|
||||
public DataStreamWebSocketServer(WebSocket webSocket, ILogger logger) {
|
||||
this.webSocket = webSocket;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public async Task<int> Run(CancellationToken cancellationToken) {
|
||||
logger.LogInformation("DataStreamWebSocketServer running");
|
||||
await foreach (var req in JsonRpcRequestAsyncEnumerable.Create(webSocket, logger, cancellationToken)) {
|
||||
await (req.Method switch {
|
||||
"subscribe" => onSubscribe(req, cancellationToken),
|
||||
"unsubscribe" => onUnsubscribe(req.Deserialize<UnsubscribeRequest>(), cancellationToken),
|
||||
_ => Task.CompletedTask,
|
||||
});
|
||||
}
|
||||
logger.LogInformation("DataStreamWebSocketServer stopped");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes internal subscriptions. To additionally notify the client of the subscription status
|
||||
/// change(s), use <see cref="DisposeAsync"/>.
|
||||
/// </summary>
|
||||
public void Dispose() {
|
||||
GC.SuppressFinalize(this);
|
||||
disposeSubscriptions();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes internal subscriptions (like <see cref="Dispose"/>), then sends 'subscription status change'
|
||||
/// notifications to the client (unlike <see cref="Dispose"/>).
|
||||
/// </summary>
|
||||
public async ValueTask DisposeAsync() {
|
||||
GC.SuppressFinalize(this);
|
||||
var finalSubs = subscriptions.Values.Select(s => (s.RequestId, s.Id)).ToList();
|
||||
disposeSubscriptions();
|
||||
// Send a 'subscription status change' msg to the client for each one.
|
||||
foreach (var (requestId, id) in finalSubs) {
|
||||
await sendSubscriptionStatus(requestId, id, "unsubscribe", SubscriptionStatus.Unsubscribed, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
async Task onSubscribe(JsonRpcRequest<JsonNode> req, CancellationToken cancellationToken) {
|
||||
ArgumentNullException.ThrowIfNull(req.Id);
|
||||
ArgumentException.ThrowIfNullOrEmpty(req.Method);
|
||||
if (!SubscribeRequest.ParseQuery(req, out var vs, out var subId, out var errMsg, out var errCode)) {
|
||||
logger.LogError("Invalid 'subscribe' request: {Reason}", errMsg);
|
||||
var rpcError = new JsonRpcError<JsonNode>() {
|
||||
Code = (long)errCode,
|
||||
Message = $"{errCode}: {errMsg}",
|
||||
};
|
||||
await send(rpcError, cancellationToken);
|
||||
return;
|
||||
}
|
||||
if (subscriptions.TryGetValue(subId, out var oldSub)) {
|
||||
oldSub.Dispose();
|
||||
}
|
||||
var sub = new DataStreamSubscription(subId, req.Id.Value, vs, webSocket, logger);
|
||||
subscriptions[subId] = sub;
|
||||
await sendSubscriptionStatus(req.Id.Value, subId, req.Method, SubscriptionStatus.Subscribed, cancellationToken);
|
||||
await sub.StartAsync(cancellationToken);
|
||||
}
|
||||
|
||||
async Task onUnsubscribe(JsonRpcRequest<UnsubscribeRequest> req, CancellationToken cancellationToken) {
|
||||
ArgumentNullException.ThrowIfNull(req.Id);
|
||||
ArgumentException.ThrowIfNullOrEmpty(req.Method);
|
||||
var subId = req.Params?.SubscriptionId ?? string.Empty;
|
||||
if (!subscriptions.TryGetValue(subId, out var sub)) {
|
||||
logger.LogInformation("Ignoring '{Method}' request because no subscription was found with id {Id}.", req.Method, subId);
|
||||
return;
|
||||
}
|
||||
logger.LogInformation("Disposing subscription");
|
||||
sub.Dispose();
|
||||
subscriptions.Remove(subId);
|
||||
await sendSubscriptionStatus(req.Id.Value, subId, req.Method, SubscriptionStatus.Unsubscribed, cancellationToken);
|
||||
}
|
||||
|
||||
async Task unsubscribe(JsonRpcId requestId, string subscriptionId, CancellationToken cancellationToken) {
|
||||
await sendSubscriptionStatus(requestId, subscriptionId, "unsubscribe", SubscriptionStatus.Unsubscribed, cancellationToken);
|
||||
}
|
||||
|
||||
Task sendSubscriptionStatus(JsonRpcId reqId, string subId, string method, SubscriptionStatus status, CancellationToken cancellationToken) {
|
||||
logger.LogInformation("Sending {Method} ack. SubscriptionId = {SubscriptionId}", method, subId);
|
||||
var response = JsonRpcResponse.Create(reqId, new SubscriptionStatusMessage() {
|
||||
SubscriptionId = subId,
|
||||
Status = status,
|
||||
});
|
||||
return send(response, cancellationToken);
|
||||
}
|
||||
|
||||
Task send<T>(T message, CancellationToken cancellationToken) {
|
||||
if (message is null) {
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
var str = JsonSerializer.Serialize(message);
|
||||
var bytes = Encoding.UTF8.GetBytes(str);
|
||||
logger.LogTrace("Sending data. Length = {Length}", bytes.Length);
|
||||
return webSocket.SendAsync(bytes, WebSocketMessageType.Text, true, cancellationToken);
|
||||
}
|
||||
|
||||
void disposeSubscriptions() {
|
||||
logger.LogInformation("Disposing DataStreamWebSocketServer subscriptions. Count: {Count}", subscriptions.Count);
|
||||
subscriptions.Values.DisposeAll();
|
||||
subscriptions.Clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Bts.DataStreamWebApp.Server.Messages;
|
||||
|
||||
enum ErrorCode {
|
||||
None = 0,
|
||||
InvalidSubscriptionRequest,
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
namespace Bts.DataStreamWebApp.Server.Messages;
|
||||
|
||||
using BCG;
|
||||
|
||||
using bts.websocket_core.json_rpc;
|
||||
|
||||
using Bts.DataStreamExpressions;
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
public class SubscribeRequest {
|
||||
[JsonPropertyName("query")]
|
||||
public string? Query { get; set; }
|
||||
|
||||
[JsonPropertyName("subscription_id")]
|
||||
public string? SubscriptionId { get; set; }
|
||||
|
||||
internal static bool ParseQuery(JsonRpcRequest<JsonNode> req, [NotNullWhen(true)] out ViewStream? stream, [NotNullWhen(true)] out string? subscriptionId, [NotNullWhen(false)] out string? errorMessage, out ErrorCode errorCode) {
|
||||
stream = null;
|
||||
subscriptionId = null;
|
||||
errorCode = ErrorCode.None;
|
||||
var paramsJsonObj = req.Params?.AsObject();
|
||||
if (paramsJsonObj is null) {
|
||||
errorCode = ErrorCode.InvalidSubscriptionRequest;
|
||||
errorMessage = "'params' object not found";
|
||||
return false;
|
||||
}
|
||||
var subReq = paramsJsonObj.Deserialize<SubscribeRequest>();
|
||||
if (subReq is null) {
|
||||
errorCode = ErrorCode.InvalidSubscriptionRequest;
|
||||
errorMessage = "'params' object invalid";
|
||||
return false;
|
||||
}
|
||||
subscriptionId = subReq.SubscriptionId ?? string.Empty;
|
||||
var query = subReq.Query;
|
||||
if (query is null) {
|
||||
errorCode = ErrorCode.InvalidSubscriptionRequest;
|
||||
errorMessage = "'params.query' field not found (expected a string)";
|
||||
return false;
|
||||
}
|
||||
errorMessage = null;
|
||||
var exp = DataStreamExpressionsInterpreter.Evaluate(query);
|
||||
if (exp is null) {
|
||||
errorCode = ErrorCode.InvalidSubscriptionRequest;
|
||||
errorMessage = "'params.query' expression has invalid syntax";
|
||||
return false;
|
||||
}
|
||||
string? exceptionMsg = null;
|
||||
try {
|
||||
stream = exp.getView(new Dictionary<string, StatementExpression>(0));
|
||||
} catch (Exception ex) {
|
||||
exceptionMsg = ex.ToString();
|
||||
}
|
||||
if (stream is null) {
|
||||
errorCode = ErrorCode.InvalidSubscriptionRequest;
|
||||
errorMessage = "Failed to create ViewStream from 'params.query' expression (check for typos in arguments/typenames)";
|
||||
if (exceptionMsg is not null) {
|
||||
errorMessage += ". Error" + exceptionMsg;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace Bts.DataStreamWebApp.Server.Messages;
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
enum SubscriptionStatus {
|
||||
None,
|
||||
Subscribed,
|
||||
Unsubscribed,
|
||||
}
|
||||
|
||||
class SubscriptionStatusMessage {
|
||||
[JsonPropertyName("subscription_id")]
|
||||
public string? SubscriptionId { get; set; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public SubscriptionStatus Status { get; set; } = SubscriptionStatus.None;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Bts.DataStreamWebApp.Server.Messages;
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
public class UnsubscribeRequest {
|
||||
[JsonPropertyName("subscription_id")]
|
||||
public string? SubscriptionId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using Bts.DataStreamCore.Services;
|
||||
using Bts.PubSub.Services;
|
||||
|
||||
using Spark.Api.Services;
|
||||
|
||||
using System.Reactive.Linq;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(new WebApplicationOptions {
|
||||
Args = args,
|
||||
ApplicationName = "Bts.DataStreamWebApp.Server",
|
||||
});
|
||||
ConfigureApp(builder.Configuration);
|
||||
ConfigureServices(builder.Services, builder.Configuration);
|
||||
|
||||
var app = builder.Build();
|
||||
if (app.Environment.IsDevelopment()) {
|
||||
app.UseDirectoryBrowser();
|
||||
}
|
||||
var wsOpts = new WebSocketOptions {
|
||||
KeepAliveInterval = TimeSpan.FromMinutes(2)
|
||||
};
|
||||
app.UseWebSockets(wsOpts);
|
||||
app.UseDefaultFiles();
|
||||
app.UseStaticFiles();
|
||||
if (app.Environment.IsDevelopment()) {
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
app.UseHttpsRedirection();
|
||||
app.UseAuthorization();
|
||||
app.MapControllers();
|
||||
try {
|
||||
Console.Write($"Starting Application");
|
||||
app.Run();
|
||||
Console.Write($"Application exited gracefully.");
|
||||
} catch (Exception ex) {
|
||||
Console.Error.Write($"Application crashed: {ex}");
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
|
||||
static void ConfigureApp(ConfigurationManager configBuilder) {
|
||||
configBuilder.AddCommandLine(Environment.GetCommandLineArgs());
|
||||
var cfgRoot = configBuilder.GetSection("config");
|
||||
if (cfgRoot?.Value is not null) {
|
||||
configBuilder.AddJsonFile(cfgRoot.Value);
|
||||
}
|
||||
}
|
||||
|
||||
static void ConfigureServices(IServiceCollection services, IConfiguration config) {
|
||||
services.AddDirectoryBrowser();
|
||||
services.AddControllers();
|
||||
services.AddEndpointsApiExplorer();
|
||||
services.AddSwaggerGen();
|
||||
services.AddMasterWorker(config.GetSection("MasterWorker"));
|
||||
services.AddViewStreamDatabase(config.GetSection("ViewStreamDatabase"));
|
||||
services.AddPubSubClient(config.GetSection("PubSub"));
|
||||
services.AddSparkd(config.GetSection("Sparkd"));
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "http://localhost:5000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy",
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5000;https://localhost:5001",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy",
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title></title>
|
||||
<style>
|
||||
table {
|
||||
border: 0
|
||||
}
|
||||
|
||||
.commslog-data {
|
||||
font-family: Consolas, Courier New, Courier, monospace;
|
||||
}
|
||||
|
||||
.commslog-server {
|
||||
background-color: red;
|
||||
color: white
|
||||
}
|
||||
|
||||
.commslog-client {
|
||||
background-color: green;
|
||||
color: white
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>ALT WebSocket Sample Application</h1>
|
||||
<p id="stateLabel">Ready to connect...</p>
|
||||
<div>
|
||||
<label for="connectionUrl">WebSocket Server URL:</label>
|
||||
<input id="connectionUrl" />
|
||||
<button id="connectButton" type="submit">Connect</button>
|
||||
</div>
|
||||
<p></p>
|
||||
<div>
|
||||
<label for="sendMessage">Message to send:</label>
|
||||
<input id="sendMessage" disabled />
|
||||
<button id="sendButton" type="submit" disabled>Send</button>
|
||||
<button id="closeButton" disabled>Close Socket</button>
|
||||
</div>
|
||||
|
||||
<h2>Communication Log</h2>
|
||||
<table style="width: 800px">
|
||||
<thead>
|
||||
<tr>
|
||||
<td style="width: 100px">From</td>
|
||||
<td style="width: 100px">To</td>
|
||||
<td>Data</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="commsLog">
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<script>
|
||||
var connectionUrl = document.getElementById("connectionUrl");
|
||||
var connectButton = document.getElementById("connectButton");
|
||||
var stateLabel = document.getElementById("stateLabel");
|
||||
var sendMessage = document.getElementById("sendMessage");
|
||||
var sendButton = document.getElementById("sendButton");
|
||||
var commsLog = document.getElementById("commsLog");
|
||||
var closeButton = document.getElementById("closeButton");
|
||||
var socket;
|
||||
|
||||
var scheme = document.location.protocol === "https:" ? "wss" : "ws";
|
||||
var port = document.location.port ? (":" + document.location.port) : "";
|
||||
|
||||
connectionUrl.value = scheme + "://" + document.location.hostname + port + "/ws" ;
|
||||
|
||||
function updateState() {
|
||||
function disable() {
|
||||
sendMessage.disabled = true;
|
||||
sendButton.disabled = true;
|
||||
closeButton.disabled = true;
|
||||
}
|
||||
function enable() {
|
||||
sendMessage.disabled = false;
|
||||
sendButton.disabled = false;
|
||||
closeButton.disabled = false;
|
||||
}
|
||||
|
||||
connectionUrl.disabled = true;
|
||||
connectButton.disabled = true;
|
||||
|
||||
if (!socket) {
|
||||
disable();
|
||||
} else {
|
||||
switch (socket.readyState) {
|
||||
case WebSocket.CLOSED:
|
||||
stateLabel.innerHTML = "Closed";
|
||||
disable();
|
||||
connectionUrl.disabled = false;
|
||||
connectButton.disabled = false;
|
||||
break;
|
||||
case WebSocket.CLOSING:
|
||||
stateLabel.innerHTML = "Closing...";
|
||||
disable();
|
||||
break;
|
||||
case WebSocket.CONNECTING:
|
||||
stateLabel.innerHTML = "Connecting...";
|
||||
disable();
|
||||
break;
|
||||
case WebSocket.OPEN:
|
||||
stateLabel.innerHTML = "Open";
|
||||
enable();
|
||||
break;
|
||||
default:
|
||||
stateLabel.innerHTML = "Unknown WebSocket State: " + htmlEscape(socket.readyState);
|
||||
disable();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
closeButton.onclick = function () {
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||
alert("socket not connected");
|
||||
}
|
||||
socket.close(1000, "Closing from client");
|
||||
};
|
||||
|
||||
sendButton.onclick = function () {
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||
alert("socket not connected");
|
||||
}
|
||||
var data = sendMessage.value;
|
||||
socket.send(data);
|
||||
commsLog.innerHTML += '<tr>' +
|
||||
'<td class="commslog-client">Client</td>' +
|
||||
'<td class="commslog-server">Server</td>' +
|
||||
'<td class="commslog-data">' + htmlEscape(data) + '</td></tr>';
|
||||
};
|
||||
|
||||
connectButton.onclick = function() {
|
||||
stateLabel.innerHTML = "Connecting";
|
||||
socket = new WebSocket(connectionUrl.value);
|
||||
socket.onopen = function (event) {
|
||||
updateState();
|
||||
commsLog.innerHTML += '<tr>' +
|
||||
'<td colspan="3" class="commslog-data">Connection opened</td>' +
|
||||
'</tr>';
|
||||
};
|
||||
socket.onclose = function (event) {
|
||||
updateState();
|
||||
commsLog.innerHTML += '<tr>' +
|
||||
'<td colspan="3" class="commslog-data">Connection closed. Code: ' + htmlEscape(event.code) + '. Reason: ' + htmlEscape(event.reason) + '</td>' +
|
||||
'</tr>';
|
||||
};
|
||||
socket.onerror = updateState;
|
||||
socket.onmessage = function (event) {
|
||||
commsLog.innerHTML += '<tr>' +
|
||||
'<td class="commslog-server">Server</td>' +
|
||||
'<td class="commslog-client">Client</td>' +
|
||||
'<td class="commslog-data">' + htmlEscape(event.data) + '</td></tr>';
|
||||
};
|
||||
};
|
||||
|
||||
function htmlEscape(str) {
|
||||
return str.toString()
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,176 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title></title>
|
||||
<style>
|
||||
table {
|
||||
border: 0
|
||||
}
|
||||
|
||||
.commslog-data {
|
||||
font-family: Consolas, Courier New, Courier, monospace;
|
||||
}
|
||||
|
||||
.commslog-server {
|
||||
background-color: red;
|
||||
color: white
|
||||
}
|
||||
|
||||
.commslog-client {
|
||||
background-color: green;
|
||||
color: white
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>DataStreams via WebSocket</h1>
|
||||
<p id="stateLabel">Ready to connect...</p>
|
||||
<div>
|
||||
<label for="connectionUrl">WebSocket Server URL:</label>
|
||||
<input id="connectionUrl" />
|
||||
<button id="connectButton" type="submit">Connect</button>
|
||||
</div>
|
||||
<p></p>
|
||||
<div>
|
||||
<label for="sendMessage">Query:</label>
|
||||
<input id="sendMessage" disabled />
|
||||
<button id="sendButton" type="submit" disabled>Send</button>
|
||||
<button id="closeButton" disabled>Close Socket</button>
|
||||
</div>
|
||||
|
||||
<h2>Communication Log</h2>
|
||||
<table style="width: 800px">
|
||||
<thead>
|
||||
<tr>
|
||||
<td style="width: 100px">From</td>
|
||||
<td style="width: 100px">To</td>
|
||||
<td>Data</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="commsLog">
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<script>
|
||||
var connectionUrl = document.getElementById("connectionUrl");
|
||||
var connectButton = document.getElementById("connectButton");
|
||||
var stateLabel = document.getElementById("stateLabel");
|
||||
var sendMessage = document.getElementById("sendMessage");
|
||||
var sendButton = document.getElementById("sendButton");
|
||||
var commsLog = document.getElementById("commsLog");
|
||||
var closeButton = document.getElementById("closeButton");
|
||||
var socket;
|
||||
|
||||
var scheme = document.location.protocol === "https:" ? "wss" : "ws";
|
||||
var port = document.location.port ? (":" + document.location.port) : "";
|
||||
|
||||
connectionUrl.value = scheme + "://" + document.location.hostname + port + "/ws" ;
|
||||
|
||||
function updateState() {
|
||||
function disable() {
|
||||
sendMessage.disabled = true;
|
||||
sendButton.disabled = true;
|
||||
closeButton.disabled = true;
|
||||
}
|
||||
function enable() {
|
||||
sendMessage.disabled = false;
|
||||
sendButton.disabled = false;
|
||||
closeButton.disabled = false;
|
||||
}
|
||||
|
||||
connectionUrl.disabled = true;
|
||||
connectButton.disabled = true;
|
||||
|
||||
if (!socket) {
|
||||
disable();
|
||||
} else {
|
||||
switch (socket.readyState) {
|
||||
case WebSocket.CLOSED:
|
||||
stateLabel.innerHTML = "Closed";
|
||||
disable();
|
||||
connectionUrl.disabled = false;
|
||||
connectButton.disabled = false;
|
||||
break;
|
||||
case WebSocket.CLOSING:
|
||||
stateLabel.innerHTML = "Closing...";
|
||||
disable();
|
||||
break;
|
||||
case WebSocket.CONNECTING:
|
||||
stateLabel.innerHTML = "Connecting...";
|
||||
disable();
|
||||
break;
|
||||
case WebSocket.OPEN:
|
||||
stateLabel.innerHTML = "Open";
|
||||
enable();
|
||||
break;
|
||||
default:
|
||||
stateLabel.innerHTML = "Unknown WebSocket State: " + htmlEscape(socket.readyState);
|
||||
disable();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
closeButton.onclick = function () {
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||
alert("socket not connected");
|
||||
}
|
||||
socket.close(1000, "Closing from client");
|
||||
};
|
||||
|
||||
sendButton.onclick = function () {
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||
alert("socket not connected");
|
||||
}
|
||||
const rpcReq = {
|
||||
jsonrpc: "2.0",
|
||||
method: "subscribe",
|
||||
params: {
|
||||
query: sendMessage.value,
|
||||
},
|
||||
id: 1,
|
||||
};
|
||||
const rpcReqStr = JSON.stringify(rpcReq);
|
||||
socket.send(rpcReqStr);
|
||||
commsLog.innerHTML += '<tr>' +
|
||||
'<td class="commslog-client">Client</td>' +
|
||||
'<td class="commslog-server">Server</td>' +
|
||||
'<td class="commslog-data">' + htmlEscape(rpcReqStr) + '</td></tr>';
|
||||
};
|
||||
|
||||
connectButton.onclick = function() {
|
||||
stateLabel.innerHTML = "Connecting";
|
||||
socket = new WebSocket(connectionUrl.value);
|
||||
socket.onopen = function (event) {
|
||||
updateState();
|
||||
commsLog.innerHTML += '<tr>' +
|
||||
'<td colspan="3" class="commslog-data">Connection opened</td>' +
|
||||
'</tr>';
|
||||
};
|
||||
socket.onclose = function (event) {
|
||||
updateState();
|
||||
commsLog.innerHTML += '<tr>' +
|
||||
'<td colspan="3" class="commslog-data">Connection closed. Code: ' + htmlEscape(event.code) + '. Reason: ' + htmlEscape(event.reason) + '</td>' +
|
||||
'</tr>';
|
||||
};
|
||||
socket.onerror = updateState;
|
||||
socket.onmessage = function (event) {
|
||||
commsLog.innerHTML += '<tr>' +
|
||||
'<td class="commslog-server">Server</td>' +
|
||||
'<td class="commslog-client">Client</td>' +
|
||||
'<td class="commslog-data">' + htmlEscape(event.data) + '</td></tr>';
|
||||
};
|
||||
};
|
||||
|
||||
function htmlEscape(str) {
|
||||
return str.toString()
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+8
-4
@@ -4,6 +4,8 @@ using System.Collections.Generic;
|
||||
using Bts.DataStreamExpressions;
|
||||
using BCG.Utilities.General;
|
||||
using bts;
|
||||
using Bts.PubSub;
|
||||
using Bts.DataStreamCore;
|
||||
|
||||
namespace BCG {
|
||||
// listens to the requests posted on "cloud_streams" request table, and
|
||||
@@ -40,13 +42,15 @@ namespace BCG {
|
||||
private IDictionary<string, StreamPubUnit>
|
||||
_stream_pub_map = new Dictionary<string, StreamPubUnit>();
|
||||
private ViewStream _stream = new TableStream("cloud_streams");
|
||||
private PubTableBroker _broker = new PubTableBroker();
|
||||
private ManagedSubscriber _sub = ManagedSubscriber.instance;
|
||||
readonly IPubTableBroker _broker;
|
||||
readonly IManagedSubscriber _sub;
|
||||
|
||||
|
||||
public RemoteQueryManager() {
|
||||
public RemoteQueryManager(IManagedSubscriber managedSubscriber, IPubTableBroker pubTableBroker) {
|
||||
_sub = managedSubscriber;
|
||||
_broker = pubTableBroker;
|
||||
_stream.onChange += _on_str;
|
||||
_sub.subscribe("RemoteRequest", (string tag, byte[] data) => {
|
||||
_sub.Subscribe("RemoteRequest", (string tag, byte[] data) => {
|
||||
RemoteRequest req = RemoteRequest.construct_from_bytes(data);
|
||||
_spawn_remote_stream(req.published_table_name, req.stream_expression, false);
|
||||
});
|
||||
@@ -1,9 +1,7 @@
|
||||
using System;
|
||||
#nullable disable
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BCG.Utilities.General;
|
||||
using BCG.Utilities;
|
||||
|
||||
namespace BCG
|
||||
{
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
using bts;
|
||||
|
||||
using Bts.DataStream;
|
||||
using Bts.DataStreamCore;
|
||||
|
||||
using mbts.config;
|
||||
using mbts.utils;
|
||||
|
||||
using System;
|
||||
|
||||
@@ -16,8 +20,8 @@ namespace BCG {
|
||||
[DataStreamUserRespresentation (false)]
|
||||
public class MapStream : MethodDrivenStream
|
||||
{
|
||||
protected IStreamMapper myMapper;
|
||||
protected IChangeableStreamMapper myChangeableMapper = null;
|
||||
protected IStreamMapper? myMapper;
|
||||
protected IChangeableStreamMapper? myChangeableMapper;
|
||||
private bool myUseNewMap = Configuration.get<DataStreamConfig>().UseNewMap;
|
||||
|
||||
public class FuncWrapper : IStreamMapper {
|
||||
@@ -72,7 +76,7 @@ namespace BCG {
|
||||
|
||||
|
||||
protected override void myHandleFirstCall() {
|
||||
|
||||
ArgumentNullException.ThrowIfNull(myMapper);
|
||||
IBlueTableSchema outputSchema = myMapper.getOutputSchema(myLastArrivedPacket.newRows.schema);
|
||||
myLastFiredPacket = new DataPacket();
|
||||
myLastFiredPacket.newRows = new BlueTable(outputSchema);
|
||||
@@ -83,28 +87,34 @@ namespace BCG {
|
||||
}
|
||||
|
||||
protected override void myHandleIncrementalAdd() {
|
||||
//t.start ();
|
||||
myLastFiredPacket.addedRows = new BlueTable(myLastFiredPacket.newRows.schema);
|
||||
foreach (IBlueRow row in myLastArrivedPacket.addedRows) {
|
||||
IBlueRow newRow = myMapper.map(row);
|
||||
(myLastFiredPacket.newRows as BlueTable).addRow(newRow);
|
||||
(myLastFiredPacket.addedRows as BlueTable).addRow(newRow);
|
||||
if (myLastArrivedPacket.addedRows.Count == 0) {
|
||||
Log.Gen.warn("MapStream.myHandleIncrementalAdd called with 0 addedRows");
|
||||
return;
|
||||
}
|
||||
//t.stop ();
|
||||
//System.Diagnostics.Trace.WriteLine ("adding " + myLastFiredPacket.addedRows.Count.ToString () + " : " + t.lastIntervalTime);
|
||||
//System.Diagnostics.Trace.WriteLine (" ");
|
||||
ArgumentNullException.ThrowIfNull(myMapper);
|
||||
var addedRows = new BlueTable(myLastFiredPacket.newRows.schema);
|
||||
var newRows = ExceptionAssert.Cast(myLastFiredPacket.newRows).To<BlueTable>();
|
||||
foreach (IBlueRow row in myLastArrivedPacket.addedRows) {
|
||||
var newRow = myMapper.map(row);
|
||||
newRows.addRow(newRow);
|
||||
addedRows.addRow(newRow);
|
||||
}
|
||||
myLastFiredPacket.addedRows = addedRows;
|
||||
}
|
||||
|
||||
protected override void myHandleIncrementalDelete() {
|
||||
//t.start ();
|
||||
myLastFiredPacket.deletedRows = new BlueTable(myLastFiredPacket.newRows.schema);
|
||||
foreach (IBlueRow row in myLastArrivedPacket.deletedRows) {
|
||||
IBlueRow delRow = (myLastFiredPacket.newRows as BlueTable)[(int)row["id"]];
|
||||
(myLastFiredPacket.deletedRows as BlueTable).addRow(delRow);
|
||||
(myLastFiredPacket.newRows as BlueTable).deleteRow(row);
|
||||
if (myLastArrivedPacket.deletedRows.Count == 0) {
|
||||
Log.Gen.warn("MapStream.myHandleIncrementalDelete called with 0 deletedRows");
|
||||
return;
|
||||
}
|
||||
//t.stop ();
|
||||
//System.Diagnostics.Trace.WriteLine ("deleting " + myLastFiredPacket.deletedRows.Count.ToString () + " : " + t.lastIntervalTime);
|
||||
var deletedRows = new BlueTable(myLastFiredPacket.newRows.schema);
|
||||
var newRows = ExceptionAssert.Cast(myLastFiredPacket.newRows).To<BlueTable>();
|
||||
foreach (IBlueRow row in myLastArrivedPacket.deletedRows) {
|
||||
var delRow = newRows[row.Id()];
|
||||
deletedRows.addRow(delRow);
|
||||
newRows.deleteRow(row);
|
||||
}
|
||||
myLastFiredPacket.deletedRows = deletedRows;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
using Bts.DataStreamCore;
|
||||
|
||||
using mbts.utils;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
using BCG.Utilities;
|
||||
using BCG.Utilities.General;
|
||||
|
||||
using mbts;
|
||||
|
||||
namespace BCG {
|
||||
/// <summary>
|
||||
@@ -56,15 +54,15 @@ namespace BCG {
|
||||
return rows == null ? new IBlueRow[] { } : new List<IBlueRow>(rows).ToArray();
|
||||
}
|
||||
|
||||
public event MapperChangeDelegate onMapperChange;
|
||||
private void _fake() { onMapperChange(); }
|
||||
public event MapperChangeDelegate? onMapperChange;
|
||||
}
|
||||
|
||||
//maps the id of the underlying stream to the
|
||||
//rows of the resulting stream
|
||||
private Dictionary<int, List<IBlueRow>> myMap = new Dictionary<int, List<IBlueRow>>();
|
||||
private int myID = 0;
|
||||
private BlueTable myRows, myBaseRows;
|
||||
private BlueTable? myRows;
|
||||
private BlueTable? myBaseRows;
|
||||
private ViewStream myStream;
|
||||
private ISuperStreamMapper myMapper;
|
||||
public bool generate_ids = true;
|
||||
@@ -92,49 +90,40 @@ namespace BCG {
|
||||
myRows = null;
|
||||
myBaseRows = null;
|
||||
myMap.Clear();
|
||||
//myMapper.onMapperChange -= new MapperChangeDelegate(myHandleMapChange);
|
||||
}
|
||||
|
||||
private void myHandleData(object newValue) {
|
||||
DataPacket idp = newValue as DataPacket;
|
||||
//HighPerformanceTimer timer = new HighPerformanceTimer();
|
||||
//timer.start();
|
||||
DataPacket dp = newValue as DataPacket;
|
||||
myRows = myRows ?? new BlueTable(myMapper.getOutputSchema(dp.schema));
|
||||
myBaseRows = myBaseRows ?? new BlueTable(dp.schema);
|
||||
var dp = ExceptionAssert.Cast(newValue).To<DataPacket>();
|
||||
myRows ??= new BlueTable(myMapper.getOutputSchema(dp.schema));
|
||||
myBaseRows ??= new BlueTable(dp.schema);
|
||||
BlueDataUtility.absorbChanges(myBaseRows, dp.deletedRows, dp.addedRows);
|
||||
BlueTable dels = new BlueTable(myRows.schema),
|
||||
adds = new BlueTable(myRows.schema);
|
||||
var dels = new BlueTable(myRows.schema);
|
||||
var adds = new BlueTable(myRows.schema);
|
||||
|
||||
foreach (IBlueRow r in dp.deletedRows) {
|
||||
List<IBlueRow> mappedRows = myMap[(int)r["id"]];
|
||||
var mappedRows = myMap[r.Id()];
|
||||
foreach (IBlueRow mr in mappedRows) {
|
||||
dels.addRow(mr);
|
||||
myRows.deleteRow(mr);
|
||||
}
|
||||
myMap.Remove((int)r["id"]);
|
||||
myMap.Remove(r.Id());
|
||||
}
|
||||
foreach (IBlueRow r in dp.addedRows) {
|
||||
IBlueRow[] mappedRows = myMapper.map(r);
|
||||
foreach (IBlueRow mr in mappedRows) {
|
||||
IBlueRow idChangedMapRow = generate_ids ? BlueDataUtility.getNewReplacedRow(mr, myID++, "id") : mr;
|
||||
if (!myMap.ContainsKey((int)r["id"])) {
|
||||
myMap.Add((int)r["id"], new List<IBlueRow>());
|
||||
}
|
||||
myMap[(int)r["id"]].Add(idChangedMapRow);
|
||||
myMap.GetOrAdd(r.Id(), _ => new()).Add(idChangedMapRow);
|
||||
myRows.addRow(idChangedMapRow);
|
||||
adds.addRow(idChangedMapRow);
|
||||
}
|
||||
}
|
||||
//timer.stop();
|
||||
// if (timer.lastIntervalTime > 20)
|
||||
// {}//mbts.Logger.log(bts.log.Level.Info, bts.log.Subsystem.gen, String.Format("SuperMapper:myHandlerData for mapper {0} took {1} msecs", myMapper.GetType().ToString(), timer.lastIntervalTime));
|
||||
myFireEvent(new DataPacket(myRows, adds, dels));
|
||||
}
|
||||
|
||||
private void myHandleMapChange() {
|
||||
if (myBaseRows != null)
|
||||
if (myBaseRows is not null) {
|
||||
myHandleData(new DataPacket(myBaseRows, new BlueTable(myBaseRows), new BlueTable(myBaseRows)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using bts.api;
|
||||
using Bts.PubSub.Routing;
|
||||
|
||||
namespace bts {
|
||||
[DataStreamUserRespresentation("pss_echo")]
|
||||
@@ -87,8 +88,10 @@ namespace bts {
|
||||
IOState _state = IOState.SEND;
|
||||
|
||||
public PubsubEchoTestStream(string server_url, TimeSpan publish_period, int payload_size, string tag_name, ViewStreamDatabase vsd) {
|
||||
var route = new PubSubRoute(server_url, ".*");
|
||||
_router = new PubSubRouter(route);
|
||||
_router = new PubSubRouter(new PubSubRouterConfig() {
|
||||
PublisherRoutes = new() { new(server_url, ".*") },
|
||||
SubscriberRoutes = new() { new(server_url, ".*") },
|
||||
});
|
||||
_vsd = vsd;
|
||||
_payload_sz = payload_size;
|
||||
var schema = BlueDataUtility.form_schema("PubSubTest",
|
||||
@@ -196,4 +199,4 @@ namespace bts {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,7 +95,7 @@ namespace BCG {
|
||||
sub_msg.serialize_into_str(str);
|
||||
str.Position = 0;
|
||||
bts.SubscribeMsg sub_msg_ = new bts.SubscribeMsg();
|
||||
sub_msg_.consruct_deserialize(str);
|
||||
sub_msg_.construct_deserialize(str);
|
||||
bool res = sub_msg.Equals(sub_msg_);
|
||||
return new TestResult {
|
||||
test_name = "SubscribeMsg",
|
||||
@@ -117,7 +117,7 @@ namespace BCG {
|
||||
pub_msg.serialize_into_str(str);
|
||||
str.Position = 0;
|
||||
var pub_msg_ = new bts.PublishMsg();
|
||||
pub_msg_.consruct_deserialize(str);
|
||||
pub_msg_.construct_deserialize(str);
|
||||
bool res = pub_msg.Equals(pub_msg_);
|
||||
return new TestResult {
|
||||
test_name = "PublishMsg",
|
||||
@@ -135,7 +135,7 @@ namespace BCG {
|
||||
msg.serialize_into_str(str);
|
||||
str.Position = 0;
|
||||
var msg_ = new bts.PubSubServerMessage();
|
||||
msg_.consruct_deserialize(str);
|
||||
msg_.construct_deserialize(str);
|
||||
bool res = msg.Equals(msg_);
|
||||
return new TestResult {
|
||||
test_name = "PubSubServerMessage",
|
||||
@@ -156,7 +156,7 @@ namespace BCG {
|
||||
msg.serialize_into_str(str);
|
||||
str.Position = 0;
|
||||
var msg_ = new PubSubClientMessage();
|
||||
msg_.consruct_deserialize(str);
|
||||
msg_.construct_deserialize(str);
|
||||
var res = msg.Equals(msg_);
|
||||
return new TestResult {
|
||||
test_name = "PubSubClientMessage",
|
||||
@@ -617,14 +617,15 @@ namespace BCG {
|
||||
|
||||
|
||||
private static TestResult _run_single_tag_data_test_clean() {
|
||||
// var tagHandlerFactory
|
||||
// create message
|
||||
var test_name = MethodBase.GetCurrentMethod().Name;
|
||||
var test_name = MethodBase.GetCurrentMethod()?.Name;
|
||||
var schema = create_schema_with_all_types();
|
||||
var tag = "DS_" + schema.tableName;
|
||||
BlueTable table = new BlueTable(schema);
|
||||
IStreamIOMsgSerializer serializer = new StreamIOSerializationController();
|
||||
IStreamMsgDeserilizer deser = new StreamIODeserializationController();
|
||||
bts.SingleTagData st_data = new bts.SingleTagData(tag, DSTagManager.DataStreamTagType);
|
||||
bts.SingleTagData st_data = new bts.SingleTagData(tag, DSTagManager.DataStreamTagType, new PubsubServerStreamTagHandler());
|
||||
// create with 100 rows
|
||||
int n_rows = 100;
|
||||
bool is_succ = true;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Bts.DataStreamCore;
|
||||
|
||||
using System;
|
||||
|
||||
namespace BCG {
|
||||
@@ -7,14 +9,15 @@ namespace BCG {
|
||||
/// </summary>
|
||||
public class CircularPublisher {
|
||||
private CircularRowCollection myBuffer;
|
||||
private PubTableBroker myBroker;
|
||||
private readonly IPubTableBroker myBroker;
|
||||
private int myBufferSize;
|
||||
public CircularPublisher(int bufferSize) {
|
||||
public CircularPublisher(int bufferSize, IPubTableBroker broker) {
|
||||
myBufferSize = bufferSize;
|
||||
myBroker = broker;
|
||||
}
|
||||
|
||||
public CircularPublisher(int bufferSize, IBlueRowCollection rows)
|
||||
: this(bufferSize) {
|
||||
public CircularPublisher(int bufferSize, IPubTableBroker broker, IBlueRowCollection rows)
|
||||
: this(bufferSize, broker) {
|
||||
publishRows(rows);
|
||||
}
|
||||
|
||||
@@ -23,7 +26,6 @@ namespace BCG {
|
||||
myBuffer = new CircularRowCollection(
|
||||
new BlueTableSchema(nr.schema.tableName, nr.schema.columnInfo),
|
||||
myBufferSize);
|
||||
myBroker = new PubTableBroker();
|
||||
myBroker.create_table(myBuffer);
|
||||
}
|
||||
IBlueRow br = myBuffer.addRowWithReturn(nr);
|
||||
@@ -38,7 +40,6 @@ namespace BCG {
|
||||
new BlueTableSchema(BlueDataUtility.extractRow(nrs, 0).schema.tableName,
|
||||
BlueDataUtility.extractRow(nrs, 0).schema.columnInfo),
|
||||
myBufferSize);
|
||||
myBroker = new PubTableBroker();
|
||||
myBroker.create_table(myBuffer);
|
||||
}
|
||||
BlueTable deletes = new BlueTable(myBuffer.schema);
|
||||
|
||||
@@ -4,10 +4,12 @@ using System.Data;
|
||||
|
||||
using BCG.Utilities;
|
||||
|
||||
using Bts.DataStreamCore;
|
||||
|
||||
namespace BCG {
|
||||
public class ViewPublisher : IDisposable {
|
||||
protected ViewPublisher() { }
|
||||
private readonly PubTableBroker _broker;
|
||||
private readonly IPubTableBroker _broker;
|
||||
protected readonly ViewStream _stream;
|
||||
private int _n_updates = 0;
|
||||
|
||||
@@ -15,7 +17,7 @@ namespace BCG {
|
||||
/// in the schema (be careful with that because this
|
||||
/// could lead to double publishes if the schema name
|
||||
/// is not uniquely chosen)
|
||||
public ViewPublisher(string name, PubTableBroker broker, ViewStream vs) {
|
||||
public ViewPublisher(string name, IPubTableBroker broker, ViewStream vs) {
|
||||
_stream = String.IsNullOrEmpty(name) ? vs : new ChangeTableNameStream(vs, name);
|
||||
_broker = broker;
|
||||
_stream.onChange += new DataStream.ChangeDelegate(_handle_update);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Bts.DataStream.Services;
|
||||
|
||||
using BCG;
|
||||
using BCG.Utilities.General;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public static class DataStreamServices {
|
||||
public static IServiceCollection AddRemoteQueryManager(this IServiceCollection services) =>
|
||||
services.AddSingleton<IWorker, RemoteQueryManager>();
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BCG {
|
||||
/// <summary>
|
||||
@@ -56,9 +57,16 @@ namespace BCG {
|
||||
private Type myColType;
|
||||
private object myColValue;
|
||||
|
||||
[JsonPropertyName("colNo")]
|
||||
public int colNo { get { return myColNo; } }
|
||||
|
||||
[JsonPropertyName("colName")]
|
||||
public string colName { get { return myColName; } }
|
||||
|
||||
[JsonPropertyName("colType")]
|
||||
public Type colType { get { return myColType; } }
|
||||
|
||||
[JsonIgnore]
|
||||
public object colValue {get{return myColValue;}}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Bts.DataStreamCore;
|
||||
|
||||
public enum DataType {
|
||||
Null,
|
||||
String,
|
||||
Float32,
|
||||
Float64,
|
||||
Int32,
|
||||
Int64,
|
||||
DateTime,
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
#nullable enable
|
||||
using BCG;
|
||||
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Bts.DataStreamCore;
|
||||
|
||||
class BlueRowJsonConverter : JsonConverter<IBlueRow> {
|
||||
IBlueTableSchema? schema = null;
|
||||
|
||||
public override IBlueRow Read(
|
||||
ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
|
||||
if (schema is null) {
|
||||
throw new InvalidOperationException("Cannot read row from json because schema is null");
|
||||
}
|
||||
var mbr = new MutableBlueRow(schema);
|
||||
int colNum = 0;
|
||||
switch (reader.TokenType) {
|
||||
case JsonTokenType.StartArray:
|
||||
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) {
|
||||
mbr.setValue(colNum++, readCellValue(ref reader));
|
||||
colNum++;
|
||||
}
|
||||
break;
|
||||
case JsonTokenType.StartObject:
|
||||
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) {
|
||||
if (reader.TokenType != JsonTokenType.PropertyName) {
|
||||
throw new InvalidOperationException(
|
||||
"Expected 'PropertyName' immediately after 'StartObject' token in row json");
|
||||
}
|
||||
var colName = reader.GetString();
|
||||
if (!reader.Read()) {
|
||||
throw new InvalidOperationException(
|
||||
"Failed to read token immediately after 'PropertyName' token in row json");
|
||||
}
|
||||
mbr.setValue(colName, readCellValue(ref reader));
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (mbr.Count != schema.columnInfo.Length) {
|
||||
throw new InvalidOperationException(
|
||||
$"Row json array length '{mbr.Count}' does not match the schema's " +
|
||||
$"column length '{schema.columnInfo.Length}'.");
|
||||
}
|
||||
return mbr;
|
||||
|
||||
static object? readCellValue(ref Utf8JsonReader reader) =>
|
||||
reader.TokenType switch {
|
||||
JsonTokenType.String => reader.GetString(),
|
||||
JsonTokenType.Number => reader.GetDouble(),
|
||||
JsonTokenType.True or JsonTokenType.False => reader.GetBoolean(),
|
||||
_ => throw new InvalidOperationException("Failed to read cell value"),
|
||||
};
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, IBlueRow value, JsonSerializerOptions options) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
#nullable enable
|
||||
using BCG;
|
||||
|
||||
using Spark.Core.Json;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Bts.DataStreamCore;
|
||||
|
||||
public class DataPacketJsonConverter : JsonConverter<DataPacket> {
|
||||
/// <summary>
|
||||
/// Whether or not to include a field for current rows (i.e., snapshot of current state). By
|
||||
/// default, only incremental changes are included ('added' and 'deleted').
|
||||
/// </summary>
|
||||
public bool IncludeCurrent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to treat rows as objects (name/value pairs), or arrays (list of values). Arrays
|
||||
/// result in smaller payloads, but aren't as easy to read as name/value pairs (for mere humans).
|
||||
/// </summary>
|
||||
public bool UseObjectRows { get; set; }
|
||||
|
||||
public override DataPacket? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
|
||||
if (!reader.ReadObject()) {
|
||||
return null;
|
||||
}
|
||||
if (!reader.ReadPropertyName(out var prop)) {
|
||||
return null;
|
||||
}
|
||||
if (!reader.Read()) {
|
||||
return null;
|
||||
}
|
||||
if (prop == "added") {
|
||||
var added = new List<MutableBlueRow>();
|
||||
if (reader.TokenType != JsonTokenType.StartArray) {
|
||||
return null;
|
||||
}
|
||||
while (true) {
|
||||
if (!reader.Read()) {
|
||||
break;
|
||||
}
|
||||
if (reader.TokenType != JsonTokenType.StartArray || !reader.Read()) {
|
||||
return null;
|
||||
}
|
||||
switch (reader.TokenType) {
|
||||
case JsonTokenType.String:
|
||||
// DAMNIT! Need the schema.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, DataPacket value, JsonSerializerOptions options) {
|
||||
using var _ = writer.WriteObject();
|
||||
using (writer.WriteArray("columns")) {
|
||||
foreach (var col in value.schema.columnInfo) {
|
||||
using (writer.WriteObject()) {
|
||||
writer.WriteString("name", col.colName);
|
||||
writer.WriteNumber("index", col.colNo);
|
||||
// TODO: use language agnostic type names (e.g., 'string', 'integer', 'float', 'date' etc.)
|
||||
writer.WriteString("type", col.type_code().ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var columns = value.schema.columnInfo.Select(c => (Column: c, Write: GetCellValueWriter(c))).ToList();
|
||||
WriteRows("added", value.addedRows);
|
||||
WriteRows("deleted", value.deletedRows);
|
||||
if (IncludeCurrent) {
|
||||
WriteRows("all", value.newRows);
|
||||
}
|
||||
|
||||
void WriteRows(string name, IBlueRowCollection rows) {
|
||||
using (writer.WriteArray(name)) {
|
||||
foreach (IBlueRow row in rows) {
|
||||
WriteRow(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WriteRow(IBlueRow row) {
|
||||
using var rowCase = UseObjectRows ? writer.WriteObject() : writer.WriteArray();
|
||||
for (int i = 0; i < columns.Count; i++) {
|
||||
if (UseObjectRows) {
|
||||
writer.WritePropertyName(columns[i].Column.colName);
|
||||
}
|
||||
columns[i].Write(row[i]);
|
||||
}
|
||||
}
|
||||
|
||||
Action<object?> GetCellValueWriter(ColumnInfo colInfo) {
|
||||
var t = colInfo.colType;
|
||||
if (t == typeof(int)) {
|
||||
return NullCheckWrapper(v => writer.WriteNumberValue((int)v));
|
||||
}
|
||||
if (t == typeof(uint)) {
|
||||
return NullCheckWrapper(v => writer.WriteNumberValue((uint)v));
|
||||
}
|
||||
if (t == typeof(long)) {
|
||||
return NullCheckWrapper(v => writer.WriteNumberValue((long)v));
|
||||
}
|
||||
if (t == typeof(ulong)) {
|
||||
return NullCheckWrapper(v => writer.WriteNumberValue((ulong)v));
|
||||
}
|
||||
if (t == typeof(short)) {
|
||||
return NullCheckWrapper(v => writer.WriteNumberValue((short)v));
|
||||
}
|
||||
if (t == typeof(ushort)) {
|
||||
return NullCheckWrapper(v => writer.WriteNumberValue((ushort)v));
|
||||
}
|
||||
if (t == typeof(double)) {
|
||||
return NullCheckWrapper(v => JsonSerializer.Serialize(writer, (double)v, options));
|
||||
}
|
||||
if (t == typeof(float)) {
|
||||
return NullCheckWrapper(v => writer.WriteNumberValue((float)v));
|
||||
}
|
||||
if (t == typeof(bool)) {
|
||||
return NullCheckWrapper(v => writer.WriteBooleanValue((bool)v));
|
||||
}
|
||||
return NullCheckWrapper(v => writer.WriteStringValue(v?.ToString()));
|
||||
|
||||
Action<object?> NullCheckWrapper(Action<object> writeNonNull) =>
|
||||
v => {
|
||||
if (v is null) {
|
||||
writer.WriteNullValue();
|
||||
} else {
|
||||
writeNonNull(v);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#nullable enable
|
||||
namespace Bts.DataStreamCore;
|
||||
|
||||
using BCG;
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// This class seriailzes DataPackets to json strings. A common buffer is used to reduce memory
|
||||
/// allocations on repeated calls to <see cref="Serialize"/>. For that reason, this class is NOT
|
||||
/// thread safe.
|
||||
/// </summary>
|
||||
public class DataPacketJsonSerializer {
|
||||
readonly MemoryStream buffer = new();
|
||||
readonly DataPacketJsonConverter converter;
|
||||
|
||||
public DataPacketJsonSerializer(DataPacketJsonConverter? converter = null) =>
|
||||
this.converter = converter ?? new() {
|
||||
IncludeCurrent = true,
|
||||
UseObjectRows = true,
|
||||
};
|
||||
|
||||
public string? Serialize(DataPacket dp) {
|
||||
buffer.Position = 0;
|
||||
buffer.SetLength(0);
|
||||
using (var jsonWriter = new Utf8JsonWriter(buffer)) {
|
||||
var opts = new JsonSerializerOptions() {
|
||||
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals
|
||||
};
|
||||
converter.Write(jsonWriter, dp, opts);
|
||||
}
|
||||
var bytes = buffer.GetBuffer();
|
||||
var span = new ArraySegment<byte>(bytes, 0, (int)buffer.Length);
|
||||
return Encoding.UTF8.GetString(span);
|
||||
}
|
||||
|
||||
public void Serialize(DataPacket dp, Utf8JsonWriter jsonWriter) {
|
||||
buffer.Position = 0;
|
||||
buffer.SetLength(0);
|
||||
var opts = new JsonSerializerOptions() {
|
||||
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals
|
||||
};
|
||||
converter.Write(jsonWriter, dp, opts);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Google.Protobuf" Version="3.24.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Bts.DataStreamCore;
|
||||
|
||||
using BCG;
|
||||
|
||||
public interface IPubTableBroker {
|
||||
void create_table(IBlueRowCollection table);
|
||||
void make_batch_changes(IBlueRowCollection new_rows, IBlueRowCollection del_rows, IBlueRowCollection added_rows);
|
||||
}
|
||||
@@ -8,12 +8,14 @@ using BCG.Utilities;
|
||||
|
||||
using bts;
|
||||
|
||||
using Bts.DataStreamCore;
|
||||
|
||||
namespace BCG {
|
||||
/// <summary>
|
||||
/// This class will be used by the clients to publish tabular data to the pubsub
|
||||
/// </summary>
|
||||
public class PubTableBroker {
|
||||
protected PublisherCore _publisher_core;
|
||||
public class PubTableBroker : IPubTableBroker {
|
||||
protected IPublisherCore _publisher_core;
|
||||
protected IDictionary<string, SinglePubTableBroker> _publisher_map =
|
||||
new Dictionary<string, SinglePubTableBroker>();
|
||||
|
||||
@@ -22,7 +24,7 @@ namespace BCG {
|
||||
public PubTableBroker()
|
||||
: this(new PublisherCore()) { }
|
||||
|
||||
public PubTableBroker(PublisherCore publisher) {
|
||||
public PubTableBroker(IPublisherCore publisher) {
|
||||
_publisher_core = publisher;
|
||||
Log.Gen.info("PubTableBroker constructed");
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@ namespace BCG {
|
||||
public class SinglePubTableBroker {
|
||||
private readonly string _tag;
|
||||
protected IStreamIOMsgSerializer _serializer;
|
||||
protected PublisherCore _publisher_core;
|
||||
protected IPublisherCore _publisher_core;
|
||||
|
||||
public SinglePubTableBroker(string tag, PublisherCore publisher_core) {
|
||||
public SinglePubTableBroker(string tag, IPublisherCore publisher_core) {
|
||||
bts.Log.Gen.info($"SinglePubTableBroker.cons created for tag: {tag}");
|
||||
_tag = tag;
|
||||
_publisher_core = publisher_core;
|
||||
@@ -51,4 +51,4 @@ namespace BCG {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-29
@@ -13,6 +13,9 @@ namespace bts {
|
||||
public PubsubServerGenericDataHandler() {
|
||||
_bw = new BinaryWriter(_str);
|
||||
}
|
||||
|
||||
public string TagType => "*";
|
||||
|
||||
public void update(string tag, byte[] msg) {
|
||||
_str.Position = 0;
|
||||
_bw.Write(msg);
|
||||
@@ -25,9 +28,6 @@ namespace bts {
|
||||
Log.Gen.info($"PubsubServerGenericDataHandler.get_content tag: {tag} rv: {rv.Length}");
|
||||
return rv;
|
||||
}
|
||||
public string[] supportedTagTypes { get; }
|
||||
public void register(InternalPublishDelegate callback) { }
|
||||
public void doWork() { }
|
||||
}
|
||||
|
||||
// This class is created by the SingleTagData for stream tag_type. Every pub
|
||||
@@ -40,6 +40,7 @@ namespace bts {
|
||||
// this is where the state of the table is kept.
|
||||
BlueTable _state = null;
|
||||
|
||||
public string TagType => DSTagManager.DataStreamTagType;
|
||||
|
||||
public void update(string tag, byte[] msg) {
|
||||
StreamIOMessage str_msg = _deser.deserialize(msg);
|
||||
@@ -74,15 +75,6 @@ namespace bts {
|
||||
Log.Gen.info($"PubsubServerSingleStreamTagHandler.get_content rv: {rv_txt}");
|
||||
return rv;
|
||||
}
|
||||
public string[] supportedTagTypes {
|
||||
get {
|
||||
return new string[] { "DS" };
|
||||
}
|
||||
}
|
||||
public void register(InternalPublishDelegate callback) {
|
||||
|
||||
}
|
||||
public void doWork() { }
|
||||
}
|
||||
|
||||
public class PubsubServerStreamTagHandler : IPubSubServerTagHandler {
|
||||
@@ -92,6 +84,9 @@ namespace bts {
|
||||
|
||||
public PubsubServerStreamTagHandler() {
|
||||
}
|
||||
|
||||
public string TagType => DSTagManager.DataStreamTagType;
|
||||
|
||||
public void update(string tag, byte[] msg) {
|
||||
BCG.IPubSubServerTagHandler rv = _get_handler(tag);
|
||||
rv.update(tag, msg);
|
||||
@@ -100,14 +95,6 @@ namespace bts {
|
||||
BCG.IPubSubServerTagHandler rv = _get_handler(tag);
|
||||
return rv.get_content(tag);
|
||||
}
|
||||
public string[] supportedTagTypes {
|
||||
get {
|
||||
List<string> rv = new List<string>();
|
||||
foreach (var h in _handler_map.Values)
|
||||
rv.AddRange(h.supportedTagTypes);
|
||||
return rv.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private BCG.IPubSubServerTagHandler _get_handler(string tag) {
|
||||
PubsubServerSingleStreamTagHandler rv = null;
|
||||
@@ -117,13 +104,5 @@ namespace bts {
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
public void register(InternalPublishDelegate callback) {
|
||||
foreach (var h in _handler_map.Values)
|
||||
h.register(callback);
|
||||
}
|
||||
public void doWork() {
|
||||
foreach (var h in _handler_map.Values)
|
||||
h.doWork();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace Bts.DataStreamCore.Services;
|
||||
|
||||
using BCG;
|
||||
using BCG.Utilities.General;
|
||||
|
||||
using Bts.PubSub.Services;
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public static class DataStreamCoreServices {
|
||||
public static IServiceCollection AddPubTableBroker(this IServiceCollection services) =>
|
||||
services.AddSingleton<IPubTableBroker, PubTableBroker>();
|
||||
|
||||
public static IServiceCollection AddViewStreamDatabase(this IServiceCollection services, IConfiguration configurationSection) =>
|
||||
services.Configure<WorkerServiceConfig>(configurationSection)
|
||||
.AddSingleton<IWorker, ViewStreamDatabase>(sp => {
|
||||
ViewStreamDatabase.SetGlobalInstance(new ViewStreamDatabase(sp.GetRequiredService<ISubscriberCore>()));
|
||||
return ViewStreamDatabase.instance;
|
||||
});
|
||||
}
|
||||
@@ -9,6 +9,7 @@ using System.Threading;
|
||||
using mbts;
|
||||
using System.Linq;
|
||||
using System.Diagnostics;
|
||||
using bts;
|
||||
|
||||
namespace BCG {
|
||||
public class ViewStreamDatabase : IWorker, IDisposable {
|
||||
@@ -230,6 +231,11 @@ namespace BCG {
|
||||
}
|
||||
}
|
||||
|
||||
internal static void SetGlobalInstance(ViewStreamDatabase vsd) {
|
||||
Log.isnull(mySelf);
|
||||
mySelf = vsd;
|
||||
}
|
||||
|
||||
private List<PendingAddRemoveWorker> _pending_worker_ops = new List<PendingAddRemoveWorker>();
|
||||
private bool _is_worker_call_in_progress = false;
|
||||
class PendingAddRemoveWorker {
|
||||
|
||||
Whitespace-only changes.
@@ -1,109 +1,120 @@
|
||||
{
|
||||
"user": "bts",
|
||||
"sparkd": "http://127.0.0.1:8000/config",
|
||||
"pubsub": {
|
||||
"milliseconds_between_sub_info_updates": 1000,
|
||||
"milliseconds_between_time_slice_updates": 5000,
|
||||
"milliseconds_between_no_buffer_space_retries": 100,
|
||||
"retries_on_no_buffer_space": 1,
|
||||
"do_it_all_seconds_between_logs": 0,
|
||||
"heartbeat_interval": 15000,
|
||||
"missing_heartbeats_before_gc": 50,
|
||||
"use_managed_sockets": true,
|
||||
"state_update_period": 5000,
|
||||
"statistics_update_period": 5000,
|
||||
"disposed_connection_check_period": 1000,
|
||||
"message_processing_warning_threshold": 1000,
|
||||
"server_port": 12345,
|
||||
"publisher_routes": [
|
||||
{
|
||||
"name": "default",
|
||||
"server_address": "bts-cloud://127.0.0.1:12345",
|
||||
"tag": ".*"
|
||||
}
|
||||
],
|
||||
"subscriber_routes": [
|
||||
{
|
||||
"name": "default",
|
||||
"server_address": "bts-cloud://127.0.0.1:12345",
|
||||
"tag": ".*"
|
||||
}
|
||||
],
|
||||
"max_packet_size": 500000000,
|
||||
"blue_comm_must_die": true,
|
||||
"crushable_tags": [],
|
||||
"tag_type_to_assembly_map": [
|
||||
{
|
||||
"handler_class": "BCG.PubSubServerTagHandler",
|
||||
"assembly": "DataStreamCore.dll"
|
||||
}
|
||||
],
|
||||
"server_queue": "PubSubQueue",
|
||||
"port_range": {
|
||||
"first": 30400,
|
||||
"last": 31700
|
||||
"Sparkd": {
|
||||
"Config": {
|
||||
"ConfigServer": "http://localhost:8000/config"
|
||||
},
|
||||
"bind_to_random_port": false,
|
||||
"debug_tags": "^NOTAGS$",
|
||||
"latency_report_threshold_ms": 1500,
|
||||
"use_substations": false,
|
||||
"use_dynamic_clients": false,
|
||||
"is_substation": false,
|
||||
"internally_publish_sub_info": true,
|
||||
"use_managed_clients_in_native_code": false,
|
||||
"compressed_tags": "^NOTAGS$",
|
||||
"tags_to_report_compression_ratio": "^NOTAGS$",
|
||||
"disable_msmq": false,
|
||||
"use_prepublished_server_state": false,
|
||||
"ip_translation": null,
|
||||
"gc_activation_threshold": 0,
|
||||
"wait_period_after_gc": 1000
|
||||
"Credentials": {
|
||||
"User": "bts",
|
||||
"Password": ""
|
||||
},
|
||||
"EnableAdmin": true,
|
||||
"EnableOrders": true,
|
||||
"EnablePosition": true
|
||||
},
|
||||
"publishers": [
|
||||
{
|
||||
"name": "market_data",
|
||||
"streams": [
|
||||
"MasterWorker": {
|
||||
"Interval": "00:00:00.1"
|
||||
},
|
||||
"Publisher": {
|
||||
"Publishers": [
|
||||
{
|
||||
"Name": "test_data",
|
||||
"Streams": [
|
||||
{
|
||||
"Name": "milliseconds_1000_100",
|
||||
"Expression": "milliseconds(3000,5)",
|
||||
"IsOnWebsocket": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"PubSub": {
|
||||
"MillisecondsBetweenSubInfoUpdates": 1000,
|
||||
"MillisecondsBetweenTimeSliceUpdates": 5000,
|
||||
"MillisecondsBetweenNoBufferSpaceRetries": 100,
|
||||
"RetriesOnNoBufferSpace": 1,
|
||||
"DoItAllSecondsBetweenLogs": 0,
|
||||
"HeartbeatInterval": 15000,
|
||||
"MissingHeartbeatsBeforeGc": 50,
|
||||
"UseManagedSockets": true,
|
||||
"StateUpdatePeriod": 5000,
|
||||
"StatisticsUpdatePeriod": 5000,
|
||||
"DisposedConnectionCheckPeriod": 1000,
|
||||
"MessageProcessingWarningThreshold": 1000,
|
||||
"ServerPort": 12345,
|
||||
"Routing": {
|
||||
"PublisherRoutes": [
|
||||
{
|
||||
"name": "bbo",
|
||||
"expression": "bbo()",
|
||||
"is_on_websocket": true
|
||||
},
|
||||
"Name": "default",
|
||||
"ServerAddress": "bts-cloud://127.0.0.1:12345",
|
||||
"TagRegExp": ".*"
|
||||
}
|
||||
],
|
||||
"SubscriberRoutes": [
|
||||
{
|
||||
"name": "instruments",
|
||||
"expression": "instruments()",
|
||||
"is_on_websocket": true
|
||||
},
|
||||
{
|
||||
"name": "timesales",
|
||||
"expression": "timesales(10000)",
|
||||
"is_on_websocket": true
|
||||
},
|
||||
{
|
||||
"name": "books",
|
||||
"expression": "books()",
|
||||
"is_on_websocket": true
|
||||
"Name": "default",
|
||||
"ServerAddress": "bts-cloud://127.0.0.1:12345",
|
||||
"TagRegExp": ".*"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "test_data",
|
||||
"streams": [
|
||||
"MaxPacketSize": 500000000,
|
||||
"BlueCommMustDie": true,
|
||||
"CrushableTags": [],
|
||||
"TagTypeToAssemblyMap": {
|
||||
"Entries": [
|
||||
{
|
||||
"name": "milliseconds_1000_100",
|
||||
"expression": "milliseconds(1000,100)",
|
||||
"is_on_websocket": true
|
||||
},
|
||||
{
|
||||
"name": "test_literal",
|
||||
"expression": "literal(\"x\", \"420.0\")",
|
||||
"is_on_websocket": true
|
||||
},
|
||||
{
|
||||
"name": "test_not_on_websocket",
|
||||
"expression": "literal(\"x\", \"840.0\")",
|
||||
"is_on_websocket": false
|
||||
"HandlerClass": "BCG.PubSubServerTagHandler",
|
||||
"Assembly": "DataStreamCore.dll"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"TagHandlerFactory": {
|
||||
"Handlers": [
|
||||
{
|
||||
"TagType": "DataStream",
|
||||
"Assembly": "DataStreamCore",
|
||||
"Class": "bts.PubsubServerStreamTagHandler"
|
||||
},
|
||||
{
|
||||
"TagType": "System_Sub",
|
||||
"Assembly": "PubSub",
|
||||
"Class": "BCG.SystemSubscriptionMapPublisher"
|
||||
},
|
||||
{
|
||||
"TagType": "System_Stats",
|
||||
"Assembly": "PubSub",
|
||||
"Class": "BCG.SystemStatsPublisher"
|
||||
},
|
||||
{
|
||||
"TagType": ".*",
|
||||
"Assembly": "DataStreamCore",
|
||||
"Class": "bts.PubsubServerGenericDataHandler"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ServerQueue": "PubSubQueue",
|
||||
"PortRange": {
|
||||
"First": 30400,
|
||||
"Last": 31700
|
||||
},
|
||||
"BindToRandomPort": false,
|
||||
"DebugTags": "^NOTAGS$",
|
||||
"LatencyReportThresholdMs": 1500,
|
||||
"UseSubstations": false,
|
||||
"UseDynamicClients": false,
|
||||
"IsSubstation": false,
|
||||
"InternallyPublishSubInfo": true,
|
||||
"UseManagedClientsInNativeCode": false,
|
||||
"CompressedTags": "^NOTAGS$",
|
||||
"TagsToReportCompressionRatio": "^NOTAGS$",
|
||||
"DisableMsmq": false,
|
||||
"UsePrepublishedServerState": false,
|
||||
"IpTranslation": null,
|
||||
"GcActivationThreshold": 0,
|
||||
"WaitPeriodAfterGc": 1000
|
||||
},
|
||||
"ViewStreamDatabase": {
|
||||
"Interval": "00:00:01"
|
||||
}
|
||||
}
|
||||
@@ -17,14 +17,14 @@
|
||||
"publisher_routes": [
|
||||
{
|
||||
"name": "default",
|
||||
"server_address": "bluto://localhost:12345",
|
||||
"server_address": "bts-cloud://localhost:12345",
|
||||
"tag": ".*"
|
||||
}
|
||||
],
|
||||
"subscriber_routes": [
|
||||
{
|
||||
"name": "default",
|
||||
"server_address": "bluto://localhost:12345",
|
||||
"server_address": "bts-cloud://localhost:12345",
|
||||
"tag": ".*"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Bts.DataStreamPublisher;
|
||||
|
||||
public class CsvPublisherConfig {
|
||||
public string? Name { get; set; }
|
||||
public string? CsvDir { get; set; }
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
namespace Bts.DataStreamPublisher;
|
||||
|
||||
using Bts.PubSub;
|
||||
|
||||
using CommandLine;
|
||||
|
||||
using System;
|
||||
@@ -11,35 +9,12 @@ using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
public class DataStreamPublisherConfig {
|
||||
/// <summary>
|
||||
/// The login user for sparkd
|
||||
/// </summary>
|
||||
[JsonPropertyName("user")]
|
||||
[Option("user", HelpText = "The sparkd login user")]
|
||||
public string? User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The login password for sparkd
|
||||
/// </summary>
|
||||
[JsonPropertyName("password")]
|
||||
[Option("pass", HelpText = "The sparkd login password")]
|
||||
public string? Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The config server url for sparkd
|
||||
/// </summary>
|
||||
[JsonPropertyName("sparkd")]
|
||||
[Option("sparkd", HelpText = "The sparkd config server")]
|
||||
public string? Sparkd { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The tables to publish on startup. Each key/value represents a table name/query to publish.
|
||||
/// </summary>
|
||||
[JsonPropertyName("publishers")]
|
||||
public List<PublisherNode>? Publishers { get; set; }
|
||||
|
||||
[JsonPropertyName("pubsub")]
|
||||
public PubSubConfig? PubSub { get; set; }
|
||||
public List<string>? CsvDirs { get; set; }
|
||||
|
||||
[Option(
|
||||
"config",
|
||||
@@ -49,16 +24,10 @@ public class DataStreamPublisherConfig {
|
||||
public string? ConfigFile { get; set; }
|
||||
|
||||
public static DataStreamPublisherConfig Default() =>
|
||||
new() {
|
||||
PubSub = PubSubConfig.Default(),
|
||||
};
|
||||
new() { };
|
||||
|
||||
public void MergeWith(DataStreamPublisherConfig other) {
|
||||
User ??= other.User;
|
||||
Password ??= other.Password;
|
||||
Sparkd ??= other.Sparkd;
|
||||
Publishers ??= other.Publishers;
|
||||
PubSub ??= other.PubSub;
|
||||
ConfigFile ??= other.ConfigFile;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,140 +1,34 @@
|
||||
using BCG;
|
||||
using BCG.Utilities.General;
|
||||
using Bts.DataStreamPublisher.Services;
|
||||
|
||||
using bts;
|
||||
using bts.api.core;
|
||||
using bts.utils;
|
||||
|
||||
using Bts.DataStreamExpressions;
|
||||
using Bts.DataStreamPublisher;
|
||||
using Bts.PubSub;
|
||||
|
||||
using CommandLine;
|
||||
|
||||
using mbts.config;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
await Parser.Default.ParseArguments<DataStreamPublisherConfig>(args)
|
||||
.MapResult(RunAsync, errs => Task.FromResult(1));
|
||||
var host = Host.CreateDefaultBuilder(args)
|
||||
.ConfigureAppConfiguration(ConfigureApp)
|
||||
.ConfigureServices(ConfigureServices)
|
||||
.Build();
|
||||
|
||||
async Task<int> RunAsync(DataStreamPublisherConfig cfg) {
|
||||
try {
|
||||
cfg.MergeWithConfigFile();
|
||||
cfg.MergeWith(DataStreamPublisherConfig.Default());
|
||||
PubSubConfig.SetGlobalConfig(Log.notnull(cfg.PubSub));
|
||||
Configuration.initialize(cfg.Sparkd);
|
||||
Log.Gen.info("Starting DataStreamPublisher");
|
||||
var service_ctx = ServiceContext.create(cfg.Sparkd);
|
||||
await service_ctx.login(cfg.User ?? "bts", cfg.Password ?? string.Empty);
|
||||
_publish_streams(cfg.Publishers ?? new(0));
|
||||
} catch (Exception ex) {
|
||||
// Write the error to console first, in case the logging system is compromised.
|
||||
var error_msg = $"DataStreamPublisher threw exception:\n{ex}";
|
||||
Console.Error.WriteLine(error_msg);
|
||||
Console.Error.Flush();
|
||||
bts.Log.Gen.error(error_msg);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
try {
|
||||
Console.Write("Starting DataStreamPublisher");
|
||||
host.Run();
|
||||
Console.Write("DataStreamPublisher exited gracefully.");
|
||||
} catch (Exception ex) {
|
||||
Console.Error.Write($"DataStreamPublisher crashed: {ex}");
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
|
||||
static ViewStream? build_stream(string? stream_expression) {
|
||||
if (stream_expression is null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
bts.Log.Gen.info($"build_stream stream_expression: {stream_expression}");
|
||||
var exp = DataStreamExpressionsInterpreter.Evaluate(stream_expression);
|
||||
var rv = exp.getView(new Dictionary<string, StatementExpression>(0));
|
||||
return rv;
|
||||
|
||||
} catch (Exception ex) {
|
||||
bts.Log.assert(false, $"Could not build stream: {stream_expression} ex: {ex}");
|
||||
return null;
|
||||
static void ConfigureApp(HostBuilderContext context, IConfigurationBuilder configBuilder) {
|
||||
configBuilder.AddCommandLine(Environment.GetCommandLineArgs());
|
||||
var cfgRoot = configBuilder.Build();
|
||||
var cfg = cfgRoot["config"];
|
||||
if (cfg is not null) {
|
||||
configBuilder.AddJsonFile(cfg, false);
|
||||
}
|
||||
}
|
||||
|
||||
void _publish_streams(IEnumerable<PublisherNode> publishers) {
|
||||
PubTableBroker pb_broker = new PubTableBroker();
|
||||
RemoteQueryManager csm = new RemoteQueryManager();
|
||||
var csvPublishers = new List<CSVPublisher>();
|
||||
|
||||
foreach (var publisher in publishers) {
|
||||
if (Directory.Exists(publisher.CsvDir)) {
|
||||
csvPublishers.Add(new(publisher.CsvDir, pb_broker));
|
||||
}
|
||||
}
|
||||
|
||||
var viewPublishers = publishers
|
||||
.SelectMany(p => p.Streams.empty_if_null().Select(createViewPublisher))
|
||||
.ToList();
|
||||
|
||||
while (true) {
|
||||
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(1));
|
||||
ViewStreamDatabase.instance.doWork();
|
||||
csm.doWork();
|
||||
MasterWorker.getInstance(false).doWork();
|
||||
ManagedSubscriber.instance.doWork();
|
||||
}
|
||||
|
||||
ViewPublisher? createViewPublisher(PublishedStream ps) {
|
||||
Log.Gen.info(
|
||||
$"Instantiating ViewPublisher for: {JsonSerializer.Serialize(ps)}");
|
||||
if (string.IsNullOrEmpty(ps.Name)) {
|
||||
Log.Gen.warn(
|
||||
$"Failed to instantiate ViewPublisher. Null/empty 'name': {JsonSerializer.Serialize(ps)}");
|
||||
return null;
|
||||
}
|
||||
var vs = build_stream(ps.Expression);
|
||||
if (vs is null) {
|
||||
Log.Gen.warn(
|
||||
$"Failed to instantiate ViewPublisher. Invalid 'expression': {JsonSerializer.Serialize(ps)}");
|
||||
return null;
|
||||
}
|
||||
var vpub = new ViewPublisher(ps.Name, pb_broker, vs);
|
||||
vs.onChange += on_change;
|
||||
return vpub;
|
||||
}
|
||||
}
|
||||
|
||||
void on_change(object obj) {
|
||||
bts.Log.Gen.info($"DSPublisher on_change dp: {obj}");
|
||||
}
|
||||
|
||||
public class CSVPublisher : IWorker {
|
||||
private IDictionary<string, ViewPublisher> _pubs = new Dictionary<string, ViewPublisher>();
|
||||
private readonly string _csv_directory;
|
||||
private PubTableBroker _pub_table_broker;
|
||||
|
||||
public CSVPublisher(string csv_directory, PubTableBroker pub_table_broker) {
|
||||
_csv_directory = csv_directory;
|
||||
_pub_table_broker = pub_table_broker;
|
||||
MasterWorker.getInstance(false).addWorker(this, TimeSpan.FromSeconds(1));
|
||||
}
|
||||
public void doWork() {
|
||||
var file_names = Directory.GetFiles(_csv_directory);
|
||||
foreach (var fn in file_names) {
|
||||
if (!_pubs.ContainsKey(fn)) {
|
||||
_handle_new_file(fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void _handle_new_file(string file_name) {
|
||||
if (file_name.EndsWith(".csv")) {
|
||||
ViewStream vs = new CSVReaderStream(file_name);
|
||||
vs = new SchemaGuesserStream(vs);
|
||||
var toks = file_name.Split('\\');
|
||||
string table_name = toks.Last();
|
||||
table_name = table_name.Remove(table_name.Length - ".csv".Length);
|
||||
ViewPublisher vp = new ViewPublisher(table_name, _pub_table_broker, vs);
|
||||
_pubs.Add(file_name, vp);
|
||||
}
|
||||
}
|
||||
}
|
||||
static void ConfigureServices(HostBuilderContext context, IServiceCollection services) =>
|
||||
services.AddDataStreamPublisher(context.Configuration);
|
||||
@@ -9,7 +9,4 @@ public class PublisherNode {
|
||||
|
||||
[JsonPropertyName("streams")]
|
||||
public List<PublishedStream>? Streams { get; set; }
|
||||
|
||||
[JsonPropertyName("csv_dir")]
|
||||
public string? CsvDir { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
namespace Bts.DataStreamPublisher.Services;
|
||||
|
||||
using BCG;
|
||||
using BCG.Utilities.General;
|
||||
|
||||
using bts;
|
||||
|
||||
using Bts.DataStreamCore;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
public class CsvPublisher : IWorker, IDisposable {
|
||||
readonly Dictionary<string, ViewPublisher> publishers = new();
|
||||
readonly string directory;
|
||||
readonly IPubTableBroker pubTableBroker;
|
||||
|
||||
public CsvPublisher(string directory, IPubTableBroker pubTableBroker) {
|
||||
this.directory = directory;
|
||||
this.pubTableBroker = pubTableBroker;
|
||||
}
|
||||
|
||||
public void doWork() {
|
||||
var newFiles = Directory.GetFiles(directory, "*.csv").WhereNot(publishers.ContainsKey);
|
||||
foreach (var file in newFiles) {
|
||||
ViewStream vs = new CSVReaderStream(file);
|
||||
vs = new SchemaGuesserStream(vs);
|
||||
var tableName = Path.GetFileNameWithoutExtension(file);
|
||||
var vp = new ViewPublisher(tableName, pubTableBroker, vs);
|
||||
publishers.Add(file, vp);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
publishers.Values.DisposeAll();
|
||||
publishers.Clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
namespace Bts.DataStreamPublisher.Services;
|
||||
|
||||
using BCG;
|
||||
|
||||
using bts;
|
||||
|
||||
using Bts.DataStreamCore;
|
||||
using Bts.DataStreamExpressions;
|
||||
using Bts.PubSub;
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using Spark.Core.Disposables;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="IHostedService"/> that publishes <see cref="ViewStream"/>s to the PubSub.
|
||||
/// It can be added to the application's services via <see cref="DataStreamPublisherServices.AddDataStreamPublisher"/>
|
||||
/// </summary>
|
||||
public class DataStreamPublisherService : IHostedService, IDisposable {
|
||||
readonly DataStreamPublisherConfig config;
|
||||
readonly IPubTableBroker pubTableBroker;
|
||||
readonly IWorkScheduler workScheduler;
|
||||
List<IDisposable>? resources = new();
|
||||
|
||||
public DataStreamPublisherService(IPubTableBroker pubTableBroker, IWorkScheduler workScheduler, IOptions<DataStreamPublisherConfig> config)
|
||||
: this(pubTableBroker, workScheduler, config.Value) { }
|
||||
|
||||
public DataStreamPublisherService(IPubTableBroker pubTableBroker, IWorkScheduler workScheduler, DataStreamPublisherConfig config) {
|
||||
this.pubTableBroker = pubTableBroker;
|
||||
this.workScheduler = workScheduler;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken) {
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
resources = new();
|
||||
foreach (var publisherNode in config.Publishers.empty_if_null()) {
|
||||
foreach (var publishedStream in publisherNode.Streams.empty_if_null()) {
|
||||
if (string.IsNullOrEmpty(publishedStream.Name)) {
|
||||
Log.Gen.warn("Skipping PublishedStream because it doesn't have a name (must define 'Name' in config json).");
|
||||
continue;
|
||||
}
|
||||
var vs = CreateViewStream(publishedStream);
|
||||
if (vs is null) {
|
||||
continue;
|
||||
}
|
||||
resources.Add(new ViewPublisher(publishedStream.Name, pubTableBroker, vs));
|
||||
}
|
||||
}
|
||||
var csvPublishers = config.CsvDirs.empty_if_null().Select(csvDir => new CsvPublisher(csvDir, pubTableBroker)).ToList();
|
||||
resources.AddRange(csvPublishers);
|
||||
resources.Add(workScheduler.Subscribe(new CompositeWorker(csvPublishers)));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) {
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
resources?.DisposeAll();
|
||||
resources?.Clear();
|
||||
}
|
||||
|
||||
static ViewStream? CreateViewStream(PublishedStream ps) {
|
||||
Log.Gen.info(
|
||||
$"Instantiating ViewPublisher for: {JsonSerializer.Serialize(ps)}");
|
||||
if (string.IsNullOrEmpty(ps.Name)) {
|
||||
Log.Gen.warn(
|
||||
$"Failed to instantiate ViewPublisher. Null/empty 'name': {JsonSerializer.Serialize(ps)}");
|
||||
return null;
|
||||
}
|
||||
var vs = buildStream(ps.Expression);
|
||||
if (vs is null) {
|
||||
Log.Gen.warn(
|
||||
$"Failed to instantiate ViewPublisher. Invalid 'expression': {JsonSerializer.Serialize(ps)}");
|
||||
return null;
|
||||
}
|
||||
return vs;
|
||||
}
|
||||
|
||||
static ViewStream? buildStream(string? streamExpression) {
|
||||
if (streamExpression is null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
Log.Gen.info($"Building ViewStream: {streamExpression}");
|
||||
var exp = DataStreamExpressionsInterpreter.Evaluate(streamExpression);
|
||||
return exp.getView(new Dictionary<string, StatementExpression>(0));
|
||||
} catch (Exception ex) {
|
||||
Log.assert(false, $"Could not build stream. expression: {streamExpression}; ex: {ex}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
namespace Bts.DataStreamPublisher.Services;
|
||||
|
||||
using BCG;
|
||||
using BCG.Utilities.General;
|
||||
|
||||
using bts;
|
||||
|
||||
using Bts.DataStream.Services;
|
||||
using Bts.DataStreamCore;
|
||||
using Bts.DataStreamCore.Services;
|
||||
using Bts.DataStreamExpressions;
|
||||
using Bts.PubSub.Services;
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
using Spark.Api.Services;
|
||||
using Spark.Core.Services;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
|
||||
public static class DataStreamPublisherServices {
|
||||
public static IServiceCollection AddDataStreamPublisher(this IServiceCollection services, IConfigurationSection configurationSection) =>
|
||||
services.Configure<DataStreamPublisherConfig>(configurationSection)
|
||||
.AddHostedService<DataStreamPublisherService>();
|
||||
|
||||
public static IServiceCollection AddCsvPublishers(this IServiceCollection services, IConfigurationSection configurationSection) {
|
||||
foreach (var csvDir in configurationSection.Get<List<string>>().empty_if_null()) {
|
||||
services.AddSingleton<IWorker, CsvPublisher>(sp => new(csvDir, sp.GetRequiredService<IPubTableBroker>()));
|
||||
}
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddDataStreamPublisher(this IServiceCollection services, IConfiguration configurationSection) {
|
||||
services.AddMasterWorker(configurationSection.GetSection("MasterWorker"))
|
||||
.AddViewStreamDatabase(configurationSection.GetSection("ViewStreamDatabase"))
|
||||
.AddPubSubClient(configurationSection.GetSection("PubSub"))
|
||||
.AddPubTableBroker()
|
||||
.AddRemoteQueryManager()
|
||||
.AddDataStreamPublisher(configurationSection.GetSection("DataStreamPublisher"))
|
||||
.AddSpark(configurationSection.GetSection("Spark"))
|
||||
.AddSparkd(configurationSection.GetSection("Sparkd"));
|
||||
return services;
|
||||
}
|
||||
|
||||
static ViewStream? buildStream(string? streamExpression) {
|
||||
if (streamExpression is null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
Log.Gen.info($"build_stream stream_expression: {streamExpression}");
|
||||
var exp = DataStreamExpressionsInterpreter.Evaluate(streamExpression);
|
||||
return exp.getView(new Dictionary<string, StatementExpression>(0));
|
||||
} catch (Exception ex) {
|
||||
Log.assert(false, $"Could not build stream. expression: {streamExpression}; ex: {ex}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -17,14 +17,14 @@
|
||||
"publisher_routes": [
|
||||
{
|
||||
"name": "default",
|
||||
"server_address": "bluto://192.168.1.80:12345",
|
||||
"server_address": "bts-cloud://192.168.1.80:12345",
|
||||
"tag": ".*"
|
||||
}
|
||||
],
|
||||
"subscriber_routes": [
|
||||
{
|
||||
"name": "default",
|
||||
"server_address": "bluto://192.168.1.80:12345",
|
||||
"server_address": "bts-cloud://192.168.1.80:12345",
|
||||
"tag": ".*"
|
||||
}
|
||||
],
|
||||
@@ -58,4 +58,4 @@
|
||||
"gc_activation_threshold": 0,
|
||||
"wait_period_after_gc": 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bts.ColumnExpressions", "Bt
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bts.ColumnSortExpressions", "Bts.ColumnSortExpressions\Bts.ColumnSortExpressions.csproj", "{B038CB72-4E81-4AF1-976F-ED8655C2C3D6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bts.DataStreamWebApp.Server", "Bts.DataStreamWebApp.Server\Bts.DataStreamWebApp.Server.csproj", "{6DE5E4CE-423E-4571-81F7-539D268D6059}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "spark_websocket", "..\client\spark_websocket\spark_websocket.csproj", "{09B0FF33-D34F-4BB4-BE96-B7D35960BB3B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -113,5 +117,13 @@ Global
|
||||
{B038CB72-4E81-4AF1-976F-ED8655C2C3D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B038CB72-4E81-4AF1-976F-ED8655C2C3D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B038CB72-4E81-4AF1-976F-ED8655C2C3D6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6DE5E4CE-423E-4571-81F7-539D268D6059}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6DE5E4CE-423E-4571-81F7-539D268D6059}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6DE5E4CE-423E-4571-81F7-539D268D6059}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6DE5E4CE-423E-4571-81F7-539D268D6059}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{09B0FF33-D34F-4BB4-BE96-B7D35960BB3B}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{09B0FF33-D34F-4BB4-BE96-B7D35960BB3B}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{09B0FF33-D34F-4BB4-BE96-B7D35960BB3B}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{09B0FF33-D34F-4BB4-BE96-B7D35960BB3B}.Release|Any CPU.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -38,7 +38,7 @@ namespace bts {
|
||||
sub_msg.serialize_into_str(str);
|
||||
str.Position = 0;
|
||||
var sub_msg_desered = new SubscribeMsg();
|
||||
sub_msg_desered.consruct_deserialize(str);
|
||||
sub_msg_desered.construct_deserialize(str);
|
||||
Assert.That(sub_msg_desered.connection, Is.EqualTo(sub_msg.connection));
|
||||
Assert.That(sub_msg_desered.subscribing_app, Is.EqualTo(sub_msg.subscribing_app));
|
||||
compare_arrays(sub_msg.tags, sub_msg_desered.tags);
|
||||
@@ -57,7 +57,7 @@ namespace bts {
|
||||
unsub_msg.serialize_into_str(str);
|
||||
str.Position = 0;
|
||||
var sub_msg_desered = new SubscribeMsg();
|
||||
sub_msg_desered.consruct_deserialize(str);
|
||||
sub_msg_desered.construct_deserialize(str);
|
||||
Assert.That(sub_msg_desered.connection, Is.EqualTo(unsub_msg.connection));
|
||||
Assert.That(sub_msg_desered.subscribing_app, Is.EqualTo(unsub_msg.subscribing_app));
|
||||
compare_arrays<string>(unsub_msg.tags, sub_msg_desered.tags);
|
||||
@@ -80,7 +80,7 @@ namespace bts {
|
||||
pub_msg.serialize_into_str(str);
|
||||
str.Position = 0;
|
||||
PublishMsg pub_msg_desered = new PublishMsg();
|
||||
pub_msg_desered.consruct_deserialize(str);
|
||||
pub_msg_desered.construct_deserialize(str);
|
||||
Assert.That(pub_msg_desered.connection, Is.EqualTo(pub_msg.connection));
|
||||
Assert.That(pub_msg_desered.publishing_app, Is.EqualTo(pub_msg.publishing_app));
|
||||
Assert.That(pub_msg_desered.tag, Is.EqualTo(pub_msg.tag));
|
||||
@@ -102,7 +102,7 @@ namespace bts {
|
||||
ps_server_msg.serialize_into_str(str);
|
||||
PubSubServerMessage ps_server_msg_desered = new PubSubServerMessage();
|
||||
str.Position = 0;
|
||||
ps_server_msg_desered.consruct_deserialize(str);
|
||||
ps_server_msg_desered.construct_deserialize(str);
|
||||
Assert.That(ps_server_msg_desered.tag, Is.EqualTo(ps_server_msg.tag));
|
||||
Assert.That(ps_server_msg_desered.tag_type, Is.EqualTo(ps_server_msg.tag_type));
|
||||
compare_arrays(ps_server_msg.data, ps_server_msg_desered.data);
|
||||
@@ -125,7 +125,7 @@ namespace bts {
|
||||
msg.serialize_into_str(str);
|
||||
PubSubClientMessage msg_deser = new PubSubClientMessage();
|
||||
str.Position = 0;
|
||||
msg_deser.consruct_deserialize(str);
|
||||
msg_deser.construct_deserialize(str);
|
||||
Assert.That(msg_deser.msg_type, Is.EqualTo(msg.msg_type));
|
||||
Assert.That(msg_deser.sub_msg, Is.EqualTo(msg.sub_msg));
|
||||
}
|
||||
|
||||
+17
-30
@@ -30,15 +30,15 @@ namespace BCG {
|
||||
public class PubSubServerManagedTCPConnection : IPubSubServerConnection,// external facing interface to communicate msgs to Pubsubserver
|
||||
ITCPMessageHandler, // to get messages from ITCPCOnnection
|
||||
ITCPConnectionHandler { // to get connections from AsyncTCPConnector_APM
|
||||
private ITCPConnection _connection;
|
||||
private ITCPConnection? _connection;
|
||||
private readonly RemoteConnectionInfo _server_rc_info;
|
||||
private readonly bool _is_sub;
|
||||
// if _data_callback is not subscribed to , the user of this class can poll updates,
|
||||
// in which case this queue will be used
|
||||
private SyncQueue<byte[]> _in_data_queue = new SyncQueue<byte[]>();
|
||||
private DateTime _last_hb = DateTime.Now;
|
||||
private PubSubServerConnectionStatusChange _conn_callback;
|
||||
private PubSubServerDataDelegate _data_callback;
|
||||
private PubSubServerConnectionStatusChange? _conn_callback;
|
||||
private PubSubServerDataDelegate? _data_callback;
|
||||
private readonly TimeSpan _connection_drop_threshold = PubSubConfig.Instance.ConnectionDropThreshold();
|
||||
private ThreadSafeQueue<ConnectionStatusChange> _conn_cb_queue = new ThreadSafeQueue<ConnectionStatusChange>();
|
||||
|
||||
@@ -51,10 +51,10 @@ namespace BCG {
|
||||
DISCONNECTED
|
||||
}
|
||||
public readonly Status connection_satus;
|
||||
public readonly ITCPConnection new_connection = null;
|
||||
public ConnectionStatusChange(Status connection_satus_, ITCPConnection new_connection_) {
|
||||
connection_satus = connection_satus_;
|
||||
new_connection = new_connection_;
|
||||
public readonly ITCPConnection? new_connection;
|
||||
public ConnectionStatusChange(Status connection_satus, ITCPConnection? new_connection) {
|
||||
this.connection_satus = connection_satus;
|
||||
this.new_connection = new_connection;
|
||||
}
|
||||
public override string ToString() {
|
||||
return connection_satus.ToString();
|
||||
@@ -101,7 +101,7 @@ namespace BCG {
|
||||
var i = 0;
|
||||
foreach (var b in rv_raw) {
|
||||
PubSubServerMessage ps_msg = new PubSubServerMessage();
|
||||
ps_msg.consruct_deserialize(b);
|
||||
ps_msg.construct_deserialize(b);
|
||||
bts.Log.Gen.info($"PubSubServerManagedTCPConnection.get_updates downloaded: {ps_msg} msg_no: {i} / {rv_raw.Count}");
|
||||
rv.Add(ps_msg);
|
||||
i++;
|
||||
@@ -162,10 +162,12 @@ namespace BCG {
|
||||
// TODO check for disconenction
|
||||
switch (message.messageType) {
|
||||
case RemoteMessageType.Data:
|
||||
if (_data_callback != null)
|
||||
ArgumentNullException.ThrowIfNull(message.message);
|
||||
if (_data_callback != null) {
|
||||
_data_callback(message.remoteConnectionInfo.ToString(), message.message);
|
||||
else
|
||||
} else {
|
||||
_in_data_queue.enqueue(message.message);
|
||||
}
|
||||
break;
|
||||
case RemoteMessageType.Heartbeat:
|
||||
_last_hb = DateTime.Now;
|
||||
@@ -200,20 +202,6 @@ namespace BCG {
|
||||
|
||||
#endregion
|
||||
|
||||
void _check_missing_hb() {
|
||||
if (!_connection.isDisposed) {
|
||||
TimeSpan hb_interval = DateTime.Now - _last_hb;
|
||||
bool hb_missed = hb_interval > _connection_drop_threshold;
|
||||
if (hb_missed) {
|
||||
bts.Log.Gen.warn("PubSubServerManagedTCPConnection._dispose_and_reconnect_if_missing_hb disposing connecion: {myServerAddress}");
|
||||
ConnectionStatusChange ci = new ConnectionStatusChange(ConnectionStatusChange.Status.DISCONNECTED, null);
|
||||
_conn_cb_queue.enqueue(ci);
|
||||
}
|
||||
} else {
|
||||
// already disposed, nothing to do
|
||||
}
|
||||
}
|
||||
|
||||
private static string _connection_txt(ITCPConnection conn) {
|
||||
if (conn != null) {
|
||||
return $"conn_type: {conn.GetType()} server: {conn.get_peer_address()} self: {conn.get_self_address()}";
|
||||
@@ -233,22 +221,22 @@ namespace BCG {
|
||||
if (_conn_cb_queue.getCount() > 0) {
|
||||
var css = _conn_cb_queue.dequeueAll();
|
||||
foreach (var c in css) {
|
||||
Log.notnull(c);
|
||||
Log.Gen.info($"PubSubServerManagedTCPConnection.doWork c: {c}");
|
||||
switch (c.connection_satus) {
|
||||
case ConnectionStatusChange.Status.CONNECTED: {
|
||||
Log.assert(c.new_connection != null);
|
||||
Log.notnull(c.new_connection);
|
||||
c.new_connection.register(this);
|
||||
_connection = c.new_connection;
|
||||
_last_hb = DateTime.Now;
|
||||
if (_conn_callback != null)
|
||||
_conn_callback(_server_rc_info.ToString(), true);
|
||||
_conn_callback?.Invoke(_server_rc_info.ToString(), true);
|
||||
break;
|
||||
}
|
||||
case ConnectionStatusChange.Status.DISCONNECTED: {
|
||||
Log.notnull(_connection);
|
||||
_connection.Dispose();
|
||||
_connect_to_ps_server();
|
||||
if (_conn_callback != null)
|
||||
_conn_callback(_server_rc_info.ToString(), false);
|
||||
_conn_callback?.Invoke(_server_rc_info.ToString(), false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -257,6 +245,5 @@ namespace BCG {
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,41 +1,23 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using BCG.Utilities.General;
|
||||
using System.Collections.Generic;
|
||||
using mbts;
|
||||
|
||||
namespace BCG {
|
||||
public delegate void InternalPublishDelegate(ICollection<Triplet<string, string, byte[]>> pubData);
|
||||
/// <summary>
|
||||
/// The Pubsub holds an instance of this for every tag
|
||||
/// type. this interface holds the state of each tag
|
||||
/// in the pubsub.
|
||||
/// </summary>
|
||||
public interface IPubSubServerTagHandler : IWorker {
|
||||
public interface IPubSubServerTagHandler {
|
||||
string TagType { get; }
|
||||
/// <summary>
|
||||
/// This is called when a new data gets published, so IPubSubServerTagHandler can
|
||||
///interpret the data and update its state (which it will be asked to return in get_content)
|
||||
/// </summary>
|
||||
/// <param name="tag"></param>
|
||||
/// <param name="messageBodies"></param>
|
||||
void update(string tag, byte[] msg);
|
||||
void update(string tag, byte[]? msg);
|
||||
/// <summary>
|
||||
/// gets the entire content of the tag
|
||||
/// </summary>
|
||||
/// <param name="tag"></param>
|
||||
/// <returns></returns>
|
||||
byte[] get_content(string tag);
|
||||
/// <summary>
|
||||
/// list of tagTypes this interface supports
|
||||
/// </summary>
|
||||
string[] supportedTagTypes { get; }
|
||||
/// <summary>
|
||||
/// registering a callback gives this interface
|
||||
/// the extra flexibility to publish its own
|
||||
/// data back to the pubsub. this function will
|
||||
/// be ignored executed outside the pubsub server
|
||||
/// </summary>
|
||||
/// <param name="callback"></param>
|
||||
void register(InternalPublishDelegate callback);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Bts.PubSub;
|
||||
|
||||
using BCG;
|
||||
|
||||
public interface IPubSubTagHandlerFactory {
|
||||
IPubSubServerTagHandler? GetHandler(string tagType);
|
||||
}
|
||||
@@ -1,364 +1,405 @@
|
||||
using BCG;
|
||||
|
||||
using bts.collections;
|
||||
using bts.utils;
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace bts {
|
||||
using BCG;
|
||||
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
|
||||
public interface IPubSubMessage {
|
||||
void serialize_into_str(Stream str);
|
||||
void consruct_deserialize(Stream str);
|
||||
}
|
||||
public static class PubSubMessageUtils {
|
||||
public static bool compare_obj<T>(T t1, T t2) {
|
||||
if (t1 == null) {
|
||||
return t2 == null;
|
||||
} else
|
||||
return t1.Equals(t2);
|
||||
}
|
||||
public static bool compare_arrays<T>(IEnumerable<T> col1, IEnumerable<T> col2) {
|
||||
if (col1.Count() != col2.Count())
|
||||
return false;
|
||||
int i = 0;
|
||||
foreach (T t in col1) {
|
||||
T t1 = t;
|
||||
T t2 = col2.ElementAt(i++);
|
||||
var eqs = compare_obj(t1, t2);
|
||||
if (!eqs)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Subscribe //////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
public class SubscribeMsg : IPubSubMessage {
|
||||
|
||||
public string connection;
|
||||
public string subscribing_app = ClientInfo.thisClient.app;
|
||||
public string[] tags;
|
||||
public void serialize_into_str(Stream str) {
|
||||
BinaryWriter bw = new BinaryWriter(str);
|
||||
bw.write_string_cpp(connection);
|
||||
bw.write_string_cpp(subscribing_app);
|
||||
bw.Write(tags.Length);
|
||||
foreach (var t in tags)
|
||||
bw.write_string_cpp(t);
|
||||
}
|
||||
|
||||
public void consruct_deserialize(Stream str) {
|
||||
BinaryReader br = new BinaryReader(str);
|
||||
this.connection = br.read_string_cpp();
|
||||
this.subscribing_app = br.read_string_cpp();
|
||||
var n_tags = br.ReadInt32();
|
||||
tags = new string[n_tags];
|
||||
for (var i = 0; i < n_tags; i++)
|
||||
tags[i] = br.read_string_cpp();
|
||||
}
|
||||
public override string ToString() {
|
||||
var tgs_str = String.Empty;
|
||||
foreach (var t in tags)
|
||||
tgs_str += t;
|
||||
return $"{GetType().Name} connection: {connection} subscribing_app: {subscribing_app} tags: {tgs_str}";
|
||||
}
|
||||
public override bool Equals(object? obj) {
|
||||
if (obj != null && obj is SubscribeMsg) {
|
||||
var obj_sub_msg = obj as SubscribeMsg;
|
||||
return PubSubMessageUtils.compare_obj(this.connection, obj_sub_msg.connection) &&
|
||||
PubSubMessageUtils.compare_obj(this.subscribing_app, obj_sub_msg.subscribing_app) &&
|
||||
PubSubMessageUtils.compare_arrays(tags, obj_sub_msg.tags);
|
||||
// TODO compare tags
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public override int GetHashCode() {
|
||||
var str = ToString();
|
||||
return str.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
// Unsubscribe ////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
public class UnsubscribeMsg : SubscribeMsg {
|
||||
}
|
||||
|
||||
// Tasnaction /////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
public class TransactionMsg : IPubSubMessage {
|
||||
uint n_msgs;
|
||||
public void serialize_into_str(Stream str) {
|
||||
BinaryWriter bw = new BinaryWriter(str);
|
||||
bw.Write(n_msgs);
|
||||
}
|
||||
public void consruct_deserialize(Stream str) {
|
||||
BinaryReader br = new BinaryReader(str);
|
||||
n_msgs = br.ReadUInt32();
|
||||
}
|
||||
public override string ToString() {
|
||||
return $"TransactionMsg n_msgs: {n_msgs}";
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj) {
|
||||
if (obj != null && (obj is TransactionMsg)) {
|
||||
TransactionMsg other_ = obj as TransactionMsg;
|
||||
return n_msgs == other_.n_msgs;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Publish ////////////////////////////////////////////////////////////////
|
||||
/// ///////////////////////////////////////////////////////////////////////
|
||||
public class PublishMsg : IPubSubMessage {
|
||||
public string connection;
|
||||
public string publishing_app = ClientInfo.thisClient.app;
|
||||
public string tag;
|
||||
public string tag_type;
|
||||
public bool is_compressed = false;
|
||||
// this is the size that of the msg, regarless of it being serialized or not
|
||||
public long publish_time_epoch_ms = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
public byte[] data_to_publish;
|
||||
|
||||
public void sanity_check() {
|
||||
Log.assert(!String.IsNullOrEmpty(tag_type));
|
||||
Log.assert(!String.IsNullOrEmpty(tag));
|
||||
Log.assert(!String.IsNullOrEmpty(connection));
|
||||
}
|
||||
|
||||
public void serialize_into_str(Stream str) {
|
||||
sanity_check();
|
||||
BinaryWriter bw = new BinaryWriter(str);
|
||||
bw.write_string_cpp(connection);
|
||||
bw.write_string_cpp(publishing_app);
|
||||
bw.write_string_cpp(tag);
|
||||
bw.write_string_cpp(tag_type);
|
||||
bw.Write(is_compressed);
|
||||
bw.Write(publish_time_epoch_ms);
|
||||
var sz = data_to_publish.Length;
|
||||
bw.Write(sz);
|
||||
bw.Write(data_to_publish);
|
||||
}
|
||||
public void consruct_deserialize(Stream str) {
|
||||
BinaryReader br = new BinaryReader(str);
|
||||
connection = br.read_string_cpp();
|
||||
publishing_app = br.read_string_cpp();
|
||||
tag = br.read_string_cpp();
|
||||
tag_type = br.read_string_cpp();
|
||||
is_compressed = br.ReadBoolean();
|
||||
publish_time_epoch_ms = br.ReadInt64();
|
||||
var size = br.ReadInt32();
|
||||
data_to_publish = br.ReadBytes(size);
|
||||
}
|
||||
public override string ToString() {
|
||||
var data_txt = StreamUtilityMethods.first_n_bytes_to_string(data_to_publish, 4);
|
||||
return $"PublishMsg connection: {connection} publishing_app: {publishing_app} " +
|
||||
$"tag: {tag} tag_type: {tag_type} is_compressed: {is_compressed} " +
|
||||
$"publish_time_epoch_ms: {publish_time_epoch_ms} data len: {data_to_publish.Length} " +
|
||||
$"data (4): {data_txt}";
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj) {
|
||||
if (obj != null && obj is PublishMsg) {
|
||||
PublishMsg pub_msg_ = obj as PublishMsg;
|
||||
return connection.Equals(pub_msg_.connection) &&
|
||||
publishing_app.Equals(pub_msg_.publishing_app) &&
|
||||
tag.Equals(pub_msg_.tag) &&
|
||||
tag_type.Equals(pub_msg_.tag_type) &&
|
||||
is_compressed.Equals(pub_msg_.is_compressed) &&
|
||||
publish_time_epoch_ms.Equals(pub_msg_.publish_time_epoch_ms) &&
|
||||
PubSubMessageUtils.compare_arrays(data_to_publish, pub_msg_.data_to_publish);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
public enum PubSubMessageType {
|
||||
SUBSCRIBE = 0,
|
||||
UNSUBSCRIIBE,
|
||||
TRANSACTION,
|
||||
PUBLISH
|
||||
}
|
||||
|
||||
public class PubSubClientMessage : IPubSubMessage {
|
||||
public PubSubMessageType msg_type;
|
||||
// only one of these wil be non null
|
||||
public SubscribeMsg sub_msg = null;
|
||||
public UnsubscribeMsg unsub_msg = null;
|
||||
public TransactionMsg transaction_msg = null;
|
||||
public PublishMsg pub_msg = null;
|
||||
|
||||
public PubSubClientMessage() { }
|
||||
|
||||
public PubSubClientMessage(SubscribeMsg sub_msg_) {
|
||||
sub_msg = sub_msg_;
|
||||
msg_type = PubSubMessageType.SUBSCRIBE;
|
||||
}
|
||||
|
||||
public PubSubClientMessage(UnsubscribeMsg unsub_msg_) {
|
||||
unsub_msg = unsub_msg_;
|
||||
msg_type = PubSubMessageType.UNSUBSCRIIBE;
|
||||
}
|
||||
|
||||
public PubSubClientMessage(TransactionMsg transaction_msg_) {
|
||||
transaction_msg = transaction_msg_;
|
||||
msg_type = PubSubMessageType.TRANSACTION;
|
||||
}
|
||||
|
||||
public PubSubClientMessage(PublishMsg pub_msg_) {
|
||||
pub_msg = pub_msg_;
|
||||
msg_type = PubSubMessageType.PUBLISH;
|
||||
}
|
||||
|
||||
public void serialize_into_str(Stream str) {
|
||||
BinaryWriter bw = new BinaryWriter(str);
|
||||
bw.Write((int)msg_type);
|
||||
switch (msg_type) {
|
||||
case PubSubMessageType.SUBSCRIBE:
|
||||
sub_msg.serialize_into_str(str);
|
||||
break;
|
||||
case PubSubMessageType.UNSUBSCRIIBE:
|
||||
unsub_msg.serialize_into_str(str);
|
||||
break;
|
||||
case PubSubMessageType.TRANSACTION:
|
||||
transaction_msg.serialize_into_str(str);
|
||||
break;
|
||||
case PubSubMessageType.PUBLISH:
|
||||
pub_msg.serialize_into_str(str);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] serialize_into_bytes() {
|
||||
MemoryStream ms = new MemoryStream();
|
||||
BinaryReader br = new BinaryReader(ms);
|
||||
serialize_into_str(ms);
|
||||
var len = Convert.ToInt32(ms.Position);
|
||||
ms.Position = 0;
|
||||
return br.ReadBytes(len);
|
||||
}
|
||||
|
||||
public void consruct_deserialize(Stream str) {
|
||||
BinaryReader br = new BinaryReader(str);
|
||||
int type = br.ReadInt32();
|
||||
msg_type = (PubSubMessageType)type;
|
||||
switch (msg_type) {
|
||||
case PubSubMessageType.SUBSCRIBE:
|
||||
sub_msg = new SubscribeMsg();
|
||||
sub_msg.consruct_deserialize(str);
|
||||
break;
|
||||
case PubSubMessageType.UNSUBSCRIIBE:
|
||||
unsub_msg = new UnsubscribeMsg();
|
||||
unsub_msg.consruct_deserialize(str);
|
||||
break;
|
||||
case PubSubMessageType.TRANSACTION:
|
||||
transaction_msg = new TransactionMsg();
|
||||
transaction_msg.consruct_deserialize(str);
|
||||
break;
|
||||
case PubSubMessageType.PUBLISH:
|
||||
pub_msg = new PublishMsg();
|
||||
pub_msg.consruct_deserialize(str);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void consruct_deserialize(byte[] msg) {
|
||||
MemoryStream ms = new MemoryStream(msg);
|
||||
ms.Position = 0;
|
||||
consruct_deserialize(ms);
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
var rv = $"type: {msg_type} ";
|
||||
switch (msg_type) {
|
||||
case PubSubMessageType.SUBSCRIBE:
|
||||
rv += $"{sub_msg}";
|
||||
break;
|
||||
case PubSubMessageType.UNSUBSCRIIBE:
|
||||
rv += $"{unsub_msg}";
|
||||
break;
|
||||
case PubSubMessageType.TRANSACTION:
|
||||
rv += $"{transaction_msg}";
|
||||
break;
|
||||
case PubSubMessageType.PUBLISH:
|
||||
rv += $"{pub_msg}";
|
||||
break;
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj) {
|
||||
if (obj != null && obj is PubSubClientMessage) {
|
||||
var msg_ = obj as PubSubClientMessage;
|
||||
return PubSubMessageUtils.compare_obj(msg_type, msg_.msg_type) &&
|
||||
PubSubMessageUtils.compare_obj(sub_msg, msg_.sub_msg) &&
|
||||
PubSubMessageUtils.compare_obj(pub_msg, msg_.pub_msg) &&
|
||||
PubSubMessageUtils.compare_obj(transaction_msg, msg_.transaction_msg) &&
|
||||
PubSubMessageUtils.compare_obj(unsub_msg, msg_.unsub_msg);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
//
|
||||
public class PubSubServerMessage : IPubSubMessage {
|
||||
public string tag;
|
||||
public string tag_type;
|
||||
public byte[] data;
|
||||
public void serialize_into_str(Stream str) {
|
||||
BinaryWriter bw = new BinaryWriter(str);
|
||||
bw.write_string_cpp(tag);
|
||||
bw.write_string_cpp(tag_type);
|
||||
bw.Write(data.Length);
|
||||
bw.Write(data);
|
||||
}
|
||||
public byte[] serialize_into_bytes() {
|
||||
MemoryStream ms = new MemoryStream();
|
||||
serialize_into_str(ms);
|
||||
var pos = Convert.ToInt32(ms.Position);
|
||||
ms.Position = 0;
|
||||
BinaryReader br = new BinaryReader(ms);
|
||||
return br.ReadBytes(pos);
|
||||
}
|
||||
public void consruct_deserialize(Stream str) {
|
||||
BinaryReader br = new BinaryReader(str);
|
||||
tag = br.read_string_cpp();
|
||||
tag_type = br.read_string_cpp();
|
||||
var data_sz = br.ReadInt32();
|
||||
data = br.ReadBytes(data_sz);
|
||||
}
|
||||
|
||||
public void consruct_deserialize(byte[] msg) {
|
||||
MemoryStream str = new MemoryStream(msg);
|
||||
str.Position = 0;
|
||||
consruct_deserialize(str);
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
var data_txt = StreamUtilityMethods.first_n_bytes_to_string(data, 4);
|
||||
return $"PubSubServerMessage tag: {tag} tag_type: {tag_type} data_len: {data.Length} " +
|
||||
$" data: {data_txt}";
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj) {
|
||||
if (obj != null && obj is PubSubServerMessage) {
|
||||
PubSubServerMessage msg_ = obj as PubSubServerMessage;
|
||||
return PubSubMessageUtils.compare_obj(tag, msg_.tag) &&
|
||||
PubSubMessageUtils.compare_obj(tag_type, msg_.tag_type) &&
|
||||
PubSubMessageUtils.compare_arrays(data, msg_.data);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
namespace bts;
|
||||
|
||||
public interface IPubSubMessage {
|
||||
void serialize_into_str(Stream str);
|
||||
void construct_deserialize(Stream str);
|
||||
}
|
||||
|
||||
public static class PubSubMessageUtils {
|
||||
public static bool compare_obj<T>(T t1, T t2) {
|
||||
if (t1 == null) {
|
||||
return t2 == null;
|
||||
} else
|
||||
return t1.Equals(t2);
|
||||
}
|
||||
public static bool compare_arrays<T>(IEnumerable<T> col1, IEnumerable<T> col2) {
|
||||
if (col1.Count() != col2.Count())
|
||||
return false;
|
||||
int i = 0;
|
||||
foreach (T t in col1) {
|
||||
T t1 = t;
|
||||
T t2 = col2.ElementAt(i++);
|
||||
var eqs = compare_obj(t1, t2);
|
||||
if (!eqs)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe //////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
public class SubscribeMsg : IPubSubMessage, IEquatable<SubscribeMsg> {
|
||||
public string? connection;
|
||||
public string subscribing_app = ClientInfo.thisClient.app;
|
||||
public string[]? tags;
|
||||
|
||||
public void serialize_into_str(Stream str) {
|
||||
if (tags is null) {
|
||||
throw new InvalidOperationException("Unable to serialize SubscribeMsg because tags is null");
|
||||
}
|
||||
var bw = new BinaryWriter(str);
|
||||
bw.write_string_cpp(connection);
|
||||
bw.write_string_cpp(subscribing_app);
|
||||
bw.Write(tags.Length);
|
||||
foreach (var t in tags) {
|
||||
bw.write_string_cpp(t);
|
||||
}
|
||||
}
|
||||
|
||||
public void construct_deserialize(Stream str) {
|
||||
var br = new BinaryReader(str);
|
||||
connection = br.read_string_cpp();
|
||||
subscribing_app = br.read_string_cpp();
|
||||
var n_tags = br.ReadInt32();
|
||||
tags = new string[n_tags];
|
||||
for (var i = 0; i < n_tags; i++) {
|
||||
tags[i] = br.read_string_cpp();
|
||||
}
|
||||
}
|
||||
|
||||
public bool Equals(SubscribeMsg? other) =>
|
||||
other is not null
|
||||
&& connection == other.connection
|
||||
&& subscribing_app == other.subscribing_app
|
||||
&& tags.safe_sequence_equals(other.tags);
|
||||
|
||||
public override bool Equals(object? obj) =>
|
||||
Equals(obj as SubscribeMsg);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
ToString().GetHashCode();
|
||||
|
||||
public override string ToString() =>
|
||||
$"{GetType().Name} connection: {connection} subscribing_app: {subscribing_app} tags: {string.Concat(tags.empty_if_null())}";
|
||||
}
|
||||
|
||||
// Unsubscribe ////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
public class UnsubscribeMsg : SubscribeMsg, IEquatable<UnsubscribeMsg> {
|
||||
public bool Equals(UnsubscribeMsg? other) =>
|
||||
other is not null && base.Equals(other);
|
||||
|
||||
public override bool Equals(object? obj) =>
|
||||
Equals(obj as UnsubscribeMsg);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
base.GetHashCode();
|
||||
}
|
||||
|
||||
// Trasnaction /////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
public class TransactionMsg : IPubSubMessage, IEquatable<TransactionMsg> {
|
||||
uint n_msgs;
|
||||
|
||||
public void serialize_into_str(Stream str) {
|
||||
var bw = new BinaryWriter(str);
|
||||
bw.Write(n_msgs);
|
||||
}
|
||||
|
||||
public void construct_deserialize(Stream str) {
|
||||
var br = new BinaryReader(str);
|
||||
n_msgs = br.ReadUInt32();
|
||||
}
|
||||
|
||||
public override string ToString() =>
|
||||
$"TransactionMsg n_msgs: {n_msgs}";
|
||||
|
||||
public bool Equals(TransactionMsg? other) =>
|
||||
n_msgs == other?.n_msgs;
|
||||
|
||||
public override bool Equals(object? obj) =>
|
||||
Equals(obj as TransactionMsg);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
n_msgs.GetHashCode();
|
||||
}
|
||||
|
||||
// Publish ////////////////////////////////////////////////////////////////
|
||||
/// ///////////////////////////////////////////////////////////////////////
|
||||
public class PublishMsg : IPubSubMessage, IEquatable<PublishMsg> {
|
||||
public string? connection;
|
||||
public string publishing_app = ClientInfo.thisClient.app;
|
||||
public string? tag;
|
||||
public string? tag_type;
|
||||
public bool is_compressed = false;
|
||||
// this is the size that of the msg, regarless of it being serialized or not
|
||||
public long publish_time_epoch_ms = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
public byte[]? data_to_publish;
|
||||
|
||||
[MemberNotNull(nameof(connection), nameof(tag), nameof(tag_type))]
|
||||
public void sanity_check() {
|
||||
Log.assert(!string.IsNullOrEmpty(tag_type));
|
||||
Log.assert(!string.IsNullOrEmpty(tag));
|
||||
Log.assert(!string.IsNullOrEmpty(connection));
|
||||
// The 'ThrowIfNullOrEmpty' calls are redundant (given the assert statements above), but
|
||||
// necessary for the compiler to recognize the strings won't be null when this method exits.
|
||||
ArgumentException.ThrowIfNullOrEmpty(tag_type);
|
||||
ArgumentException.ThrowIfNullOrEmpty(tag);
|
||||
ArgumentException.ThrowIfNullOrEmpty(connection);
|
||||
}
|
||||
|
||||
public void serialize_into_str(Stream str) {
|
||||
sanity_check();
|
||||
if (data_to_publish is null) {
|
||||
throw new InvalidOperationException("Unable to serialize PublishMsg because data_to_publish is null");
|
||||
}
|
||||
var bw = new BinaryWriter(str);
|
||||
bw.write_string_cpp(connection);
|
||||
bw.write_string_cpp(publishing_app);
|
||||
bw.write_string_cpp(tag);
|
||||
bw.write_string_cpp(tag_type);
|
||||
bw.Write(is_compressed);
|
||||
bw.Write(publish_time_epoch_ms);
|
||||
var sz = data_to_publish.Length;
|
||||
bw.Write(sz);
|
||||
bw.Write(data_to_publish);
|
||||
}
|
||||
|
||||
public void construct_deserialize(Stream str) {
|
||||
var br = new BinaryReader(str);
|
||||
connection = br.read_string_cpp();
|
||||
publishing_app = br.read_string_cpp();
|
||||
tag = br.read_string_cpp();
|
||||
tag_type = br.read_string_cpp();
|
||||
is_compressed = br.ReadBoolean();
|
||||
publish_time_epoch_ms = br.ReadInt64();
|
||||
var size = br.ReadInt32();
|
||||
data_to_publish = br.ReadBytes(size);
|
||||
}
|
||||
|
||||
public bool Equals(PublishMsg? other) =>
|
||||
other is not null
|
||||
&& connection == other.connection
|
||||
&& publishing_app == other.publishing_app
|
||||
&& tag == other.tag
|
||||
&& tag_type == other.tag_type
|
||||
&& is_compressed == other.is_compressed
|
||||
&& publish_time_epoch_ms == other.publish_time_epoch_ms
|
||||
&& data_to_publish.safe_sequence_equals(other.data_to_publish);
|
||||
|
||||
public override bool Equals(object? obj) =>
|
||||
Equals(obj as PublishMsg);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
HashCode.Combine(
|
||||
connection,
|
||||
publishing_app,
|
||||
tag,
|
||||
is_compressed,
|
||||
publish_time_epoch_ms,
|
||||
data_to_publish?.Length);
|
||||
|
||||
public override string ToString() {
|
||||
var data_txt = StreamUtilityMethods.first_n_bytes_to_string(data_to_publish ?? Array.Empty<byte>(), 4);
|
||||
return $"PublishMsg connection: {connection} publishing_app: {publishing_app} "
|
||||
+ $"tag: {tag} tag_type: {tag_type} is_compressed: {is_compressed} "
|
||||
+ $"publish_time_epoch_ms: {publish_time_epoch_ms} data len: {data_to_publish?.Length ?? 0} "
|
||||
+ $"data (4): {data_txt}";
|
||||
}
|
||||
}
|
||||
|
||||
public enum PubSubMessageType {
|
||||
SUBSCRIBE = 0,
|
||||
UNSUBSCRIIBE,
|
||||
TRANSACTION,
|
||||
PUBLISH
|
||||
}
|
||||
|
||||
public class PubSubClientMessage : IPubSubMessage {
|
||||
public PubSubMessageType msg_type;
|
||||
// only one of these wil be non null
|
||||
public SubscribeMsg? sub_msg;
|
||||
public UnsubscribeMsg? unsub_msg;
|
||||
public TransactionMsg? transaction_msg;
|
||||
public PublishMsg? pub_msg;
|
||||
|
||||
public PubSubClientMessage() { }
|
||||
|
||||
public PubSubClientMessage(SubscribeMsg sub_msg_) {
|
||||
sub_msg = sub_msg_;
|
||||
msg_type = PubSubMessageType.SUBSCRIBE;
|
||||
}
|
||||
|
||||
public PubSubClientMessage(UnsubscribeMsg unsub_msg_) {
|
||||
unsub_msg = unsub_msg_;
|
||||
msg_type = PubSubMessageType.UNSUBSCRIIBE;
|
||||
}
|
||||
|
||||
public PubSubClientMessage(TransactionMsg transaction_msg_) {
|
||||
transaction_msg = transaction_msg_;
|
||||
msg_type = PubSubMessageType.TRANSACTION;
|
||||
}
|
||||
|
||||
public PubSubClientMessage(PublishMsg pub_msg_) {
|
||||
pub_msg = pub_msg_;
|
||||
msg_type = PubSubMessageType.PUBLISH;
|
||||
}
|
||||
|
||||
public void serialize_into_str(Stream str) {
|
||||
var bw = new BinaryWriter(str);
|
||||
bw.Write((int)msg_type);
|
||||
switch (msg_type) {
|
||||
case PubSubMessageType.SUBSCRIBE:
|
||||
ValidateMsg(sub_msg).serialize_into_str(str);
|
||||
break;
|
||||
case PubSubMessageType.UNSUBSCRIIBE:
|
||||
ValidateMsg(unsub_msg).serialize_into_str(str);
|
||||
break;
|
||||
case PubSubMessageType.TRANSACTION:
|
||||
ValidateMsg(transaction_msg).serialize_into_str(str);
|
||||
break;
|
||||
case PubSubMessageType.PUBLISH:
|
||||
ValidateMsg(pub_msg).serialize_into_str(str);
|
||||
break;
|
||||
|
||||
T ValidateMsg<T>([NotNull]T? msg, [CallerArgumentExpression(nameof(msg))]string name = "") {
|
||||
if (msg is null) {
|
||||
throw new InvalidOperationException($"msg type is {msg_type} but {name} is null");
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] serialize_into_bytes() {
|
||||
var ms = new MemoryStream();
|
||||
var br = new BinaryReader(ms);
|
||||
serialize_into_str(ms);
|
||||
var len = Convert.ToInt32(ms.Position);
|
||||
ms.Position = 0;
|
||||
return br.ReadBytes(len);
|
||||
}
|
||||
|
||||
public void construct_deserialize(Stream str) {
|
||||
var br = new BinaryReader(str);
|
||||
int type = br.ReadInt32();
|
||||
msg_type = (PubSubMessageType)type;
|
||||
switch (msg_type) {
|
||||
case PubSubMessageType.SUBSCRIBE:
|
||||
sub_msg = new SubscribeMsg();
|
||||
sub_msg.construct_deserialize(str);
|
||||
break;
|
||||
case PubSubMessageType.UNSUBSCRIIBE:
|
||||
unsub_msg = new UnsubscribeMsg();
|
||||
unsub_msg.construct_deserialize(str);
|
||||
break;
|
||||
case PubSubMessageType.TRANSACTION:
|
||||
transaction_msg = new TransactionMsg();
|
||||
transaction_msg.construct_deserialize(str);
|
||||
break;
|
||||
case PubSubMessageType.PUBLISH:
|
||||
pub_msg = new PublishMsg();
|
||||
pub_msg.construct_deserialize(str);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void consruct_deserialize(byte[] msg) =>
|
||||
construct_deserialize(new MemoryStream(msg) {
|
||||
Position = 0
|
||||
});
|
||||
|
||||
public bool Equals(PubSubClientMessage? other) =>
|
||||
other is not null
|
||||
&& msg_type == other.msg_type
|
||||
&& sub_msg.safe_equals(other.sub_msg)
|
||||
&& pub_msg.safe_equals(other.pub_msg)
|
||||
&& transaction_msg.safe_equals(other.transaction_msg)
|
||||
&& unsub_msg.safe_equals(other.unsub_msg);
|
||||
|
||||
public override bool Equals(object? obj) =>
|
||||
Equals(obj as PubSubClientMessage);
|
||||
|
||||
public override int GetHashCode() {
|
||||
var msgHash = msg_type switch {
|
||||
PubSubMessageType.SUBSCRIBE => sub_msg?.GetHashCode(),
|
||||
PubSubMessageType.UNSUBSCRIIBE => unsub_msg?.GetHashCode(),
|
||||
PubSubMessageType.TRANSACTION => transaction_msg?.GetHashCode(),
|
||||
PubSubMessageType.PUBLISH => pub_msg?.GetHashCode(),
|
||||
_ => 0,
|
||||
};
|
||||
return HashCode.Combine(msg_type, msgHash);
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
var rv = $"type: {msg_type} ";
|
||||
rv += msg_type switch {
|
||||
PubSubMessageType.SUBSCRIBE => $"{sub_msg}",
|
||||
PubSubMessageType.UNSUBSCRIIBE => $"{sub_msg}",
|
||||
PubSubMessageType.TRANSACTION => $"{sub_msg}",
|
||||
PubSubMessageType.PUBLISH => $"{sub_msg}",
|
||||
_ => "Invalid msg type",
|
||||
};
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
|
||||
public class PubSubServerMessage : IPubSubMessage, IEquatable<PubSubServerMessage> {
|
||||
public string? tag;
|
||||
public string? tag_type;
|
||||
public byte[]? data;
|
||||
|
||||
public void serialize_into_str(Stream str) {
|
||||
if (data is null) {
|
||||
throw new InvalidOperationException("Unable to serialize message because data is null");
|
||||
}
|
||||
var bw = new BinaryWriter(str);
|
||||
bw.write_string_cpp(tag);
|
||||
bw.write_string_cpp(tag_type);
|
||||
bw.Write(data.Length);
|
||||
bw.Write(data);
|
||||
}
|
||||
|
||||
public byte[] serialize_into_bytes() {
|
||||
var ms = new MemoryStream();
|
||||
serialize_into_str(ms);
|
||||
var pos = Convert.ToInt32(ms.Position);
|
||||
ms.Position = 0;
|
||||
var br = new BinaryReader(ms);
|
||||
return br.ReadBytes(pos);
|
||||
}
|
||||
|
||||
[MemberNotNull(nameof(tag), nameof(tag_type))]
|
||||
public void construct_deserialize(Stream str) {
|
||||
var br = new BinaryReader(str);
|
||||
tag = br.read_string_cpp();
|
||||
tag_type = br.read_string_cpp();
|
||||
var data_sz = br.ReadInt32();
|
||||
data = br.ReadBytes(data_sz);
|
||||
}
|
||||
|
||||
[MemberNotNull(nameof(tag), nameof(tag_type))]
|
||||
public void construct_deserialize(byte[] msg) =>
|
||||
construct_deserialize(new MemoryStream(msg) { Position = 0});
|
||||
|
||||
public bool Equals(PubSubServerMessage? other) =>
|
||||
other is not null
|
||||
&& tag == other.tag
|
||||
&& tag_type == other.tag_type
|
||||
&& PubSubMessageUtils.compare_arrays(data.empty_if_null(), other.data.empty_if_null());
|
||||
|
||||
public override bool Equals(object? obj) =>
|
||||
Equals(obj as PubSubServerMessage);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
HashCode.Combine(tag, tag_type, data?.Length);
|
||||
|
||||
public override string ToString() {
|
||||
var data_txt = StreamUtilityMethods.first_n_bytes_to_string(data ?? Array.Empty<byte>(), 4);
|
||||
return $"PubSubServerMessage tag: {tag} tag_type: {tag_type} data_len: {data?.Length ?? 0} " +
|
||||
$" data: {data_txt}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
namespace Bts.PubSub;
|
||||
|
||||
using BCG;
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class PubSubTagTypeHandlerConfig {
|
||||
public string? TagType { get; set; }
|
||||
public string? Assembly { get; set; }
|
||||
public string? Class { get; set; }
|
||||
}
|
||||
|
||||
public class PubSubTagHandlerFactoryConfig {
|
||||
public List<PubSubTagTypeHandlerConfig>? Handlers { get; set; }
|
||||
|
||||
public static PubSubTagHandlerFactoryConfig Default() {
|
||||
return new() {
|
||||
Handlers = new() {
|
||||
new() {
|
||||
TagType = string.Empty,
|
||||
Assembly = "DataStreamCore",
|
||||
Class = "bts.PubsubServerStreamTagHandler",
|
||||
},
|
||||
new() {
|
||||
TagType = DSTagManager.DataStreamTagType,
|
||||
Assembly = "DataStreamCore",
|
||||
Class = "bts.PubsubServerStreamTagHandler",
|
||||
},
|
||||
new() {
|
||||
TagType = "System_Sub",
|
||||
Assembly = "PubSub",
|
||||
Class = "BCG.SystemSubscriptionMapPublisher",
|
||||
},
|
||||
new() {
|
||||
TagType = "System_Stats",
|
||||
Assembly = "PubSub",
|
||||
Class = "BCG.SystemStatsPublisher",
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,76 +1,106 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Reflection;
|
||||
using BCG.Utilities;
|
||||
using System.Collections.Generic;
|
||||
using BCG;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.IO;
|
||||
|
||||
|
||||
using System.Linq;
|
||||
using Bts.PubSub;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace bts {
|
||||
public static class PubsubTagHandlerFactory {
|
||||
// each tag type is represented by this
|
||||
public class TagHandlerEntry {
|
||||
public readonly string tag_type;
|
||||
public readonly string assembly_name;
|
||||
public readonly string handler_class_name;
|
||||
public TagHandlerEntry(string tag_type_, string assembly_name_, string handler_class_name_) {
|
||||
tag_type = tag_type_;
|
||||
assembly_name = assembly_name_;
|
||||
handler_class_name = handler_class_name_;
|
||||
}
|
||||
public class PubsubTagHandlerFactory : IPubSubTagHandlerFactory {
|
||||
readonly List<TagTypeHandlerData> handlers;
|
||||
readonly IServiceProvider serviceProvider;
|
||||
|
||||
public PubsubTagHandlerFactory(IServiceProvider serviceProvider, IOptions<PubSubTagHandlerFactoryConfig> config)
|
||||
: this(serviceProvider, config.Value) { }
|
||||
|
||||
public PubsubTagHandlerFactory(IServiceProvider serviceProvider, PubSubTagHandlerFactoryConfig config) {
|
||||
this.serviceProvider = serviceProvider;
|
||||
Log.notnull(config.Handlers);
|
||||
handlers = config.Handlers.Select(h => new TagTypeHandlerData(h)).ToList();
|
||||
}
|
||||
private static Dictionary<string, TagHandlerEntry> _config_map = new Dictionary<string, TagHandlerEntry>()
|
||||
{
|
||||
{String.Empty, new TagHandlerEntry(DSTagManager.DataStreamTagType , "DataStreamCore", "bts.PubsubServerStreamTagHandler")},
|
||||
{DSTagManager.DataStreamTagType, new TagHandlerEntry(DSTagManager.DataStreamTagType , "DataStreamCore", "bts.PubsubServerStreamTagHandler")},
|
||||
{"System_Sub", new TagHandlerEntry("System_Sub", "PubSub", "BCG.SystemSubscriptionMapPublisher")},
|
||||
{"System_Stats", new TagHandlerEntry("System_Stats", "PubSub", "BCG.SystemStatsPublisher")}
|
||||
|
||||
public IPubSubServerTagHandler? GetHandler(string tag_type) {
|
||||
if (_handler_map.TryGetValue(tag_type, out var handler)) {
|
||||
return handler;
|
||||
}
|
||||
return get_handler(tag_type, true);
|
||||
}
|
||||
|
||||
private static PubSubTagTypeHandlerConfig _wildcard_handler = new() {
|
||||
TagType = "*",
|
||||
Assembly = "DataStreamCore",
|
||||
Class = "bts.PubsubServerGenericDataHandler",
|
||||
};
|
||||
private static TagHandlerEntry _wildcard_handler = new TagHandlerEntry("*", "DataStreamCore", "bts.PubsubServerGenericDataHandler");
|
||||
private static Dictionary<string, IPubSubServerTagHandler> _handler_map = new Dictionary<string, IPubSubServerTagHandler>();
|
||||
private Dictionary<string, IPubSubServerTagHandler> _handler_map = new Dictionary<string, IPubSubServerTagHandler>();
|
||||
|
||||
public static string assembly_location => Assembly.GetExecutingAssembly().Location;
|
||||
public string assembly_location => Assembly.GetExecutingAssembly().Location;
|
||||
|
||||
public static BCG.IPubSubServerTagHandler get_handler(string tag_type, bool on_pubsub_server) {
|
||||
TagHandlerEntry handler_entry = _find_config_entry(tag_type);
|
||||
Log.assert(handler_entry != null);
|
||||
return _find_or_build_handler(handler_entry);
|
||||
public BCG.IPubSubServerTagHandler get_handler(string tag_type, bool on_pubsub_server) {
|
||||
var handler_entry = _find_config_entry(tag_type);
|
||||
return _find_or_build_handler(handler_entry.Config);
|
||||
}
|
||||
|
||||
private static TagHandlerEntry _find_config_entry(string tag_type) {
|
||||
TagHandlerEntry rv = null;
|
||||
if (_config_map.TryGetValue(tag_type, out rv))
|
||||
return rv;
|
||||
else
|
||||
return _wildcard_handler;
|
||||
}
|
||||
|
||||
private static BCG.IPubSubServerTagHandler _find_or_build_handler(TagHandlerEntry entry) {
|
||||
IPubSubServerTagHandler rv = null;
|
||||
if (_handler_map.TryGetValue(entry.tag_type, out rv)) {
|
||||
return rv;
|
||||
private TagTypeHandlerData _find_config_entry(string tag_type) {
|
||||
// Look for an exact match.
|
||||
foreach (var handler in handlers) {
|
||||
if (handler.TagType == tag_type) {
|
||||
return handler;
|
||||
}
|
||||
}
|
||||
{
|
||||
// If an exact match wasn't found, look for a regex match
|
||||
foreach (var handler in handlers) {
|
||||
if (handler.TagTypeRegex.IsMatch(tag_type)) {
|
||||
return handler;
|
||||
}
|
||||
}
|
||||
Log.assert(false);
|
||||
throw new InvalidOperationException($"Failed to handler config for tag type '{tag_type}'");
|
||||
}
|
||||
|
||||
private BCG.IPubSubServerTagHandler _find_or_build_handler(PubSubTagTypeHandlerConfig entry) {
|
||||
ArgumentNullException.ThrowIfNull(entry.TagType);
|
||||
lock (_handler_map) {
|
||||
if (_handler_map.TryGetValue(entry.TagType, out var rv)) {
|
||||
return rv;
|
||||
}
|
||||
rv = _build_handler(entry);
|
||||
_handler_map.Add(entry.tag_type, rv);
|
||||
_handler_map.Add(entry.TagType, rv);
|
||||
return rv;
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
private static BCG.IPubSubServerTagHandler _build_handler(TagHandlerEntry entry) {
|
||||
private BCG.IPubSubServerTagHandler _build_handler(PubSubTagTypeHandlerConfig entry) {
|
||||
Log.notnull(entry.Assembly);
|
||||
Log.notnull(entry.Class);
|
||||
var ass_loc = assembly_location;
|
||||
bts.Log.Gen.info($"PubsubTagHandlerFactory.load_handler entry: {entry} assembly_location: {ass_loc}");
|
||||
var assem = Assembly.LoadFrom(Path.ChangeExtension(entry.assembly_name, "dll"))
|
||||
var assem = Assembly.LoadFrom(Path.ChangeExtension(entry.Assembly, "dll"))
|
||||
?? throw new ArgumentException("Failed to instantiate TagHandlerEntry");
|
||||
bts.Log.Gen.info($"PubsubTagHandlerFactory.load_handler successfully loaded assembly: {assem}");
|
||||
Type t = assem.GetType(entry.handler_class_name, true, true);
|
||||
object obj = Activator.CreateInstance(t);
|
||||
BCG.IPubSubServerTagHandler tag_handler = obj as BCG.IPubSubServerTagHandler;
|
||||
bts.Log.Gen.info($"PubsubTagHandlerFactory.load_handler successfully created obj: {obj} of type: {t} from assem: {assem}");
|
||||
var t = assem.GetType(entry.Class, true, true);
|
||||
Log.notnull(t);
|
||||
var obj = ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider, t);
|
||||
var tag_handler = Log.notnull(obj as IPubSubServerTagHandler);
|
||||
Log.Gen.info($"PubsubTagHandlerFactory created handler. {JsonSerializer.Serialize(entry)}");
|
||||
return tag_handler;
|
||||
}
|
||||
|
||||
readonly struct TagTypeHandlerData {
|
||||
public TagTypeHandlerData(PubSubTagTypeHandlerConfig config) {
|
||||
Config = config;
|
||||
Log.notnull(config.TagType);
|
||||
TagType = config.TagType;
|
||||
TagTypeRegex = new(TagType);
|
||||
}
|
||||
|
||||
public PubSubTagTypeHandlerConfig Config { get; }
|
||||
public string TagType { get; }
|
||||
public Regex TagTypeRegex { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,104 +1,67 @@
|
||||
using bts;
|
||||
|
||||
using mbts.utils;
|
||||
|
||||
using System;
|
||||
using BCG;
|
||||
//using BCG.Utilities;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace BCG {
|
||||
[JsonConverter(typeof(PubSubRouteJsonConverter))]
|
||||
public class PubSubRoute : IComparable<PubSubRoute> {
|
||||
public string server_address;
|
||||
public string tag_reg_exp;
|
||||
//route names are used in intermediary code (i.e. while substations are being deployed)
|
||||
//so that MC Hammer can advertise them.
|
||||
public string name;
|
||||
private Regex _regex = null;
|
||||
namespace BCG;
|
||||
|
||||
public PubSubRoute() { }
|
||||
public class PubSubRoute : IEquatable<PubSubRoute>, IComparable<PubSubRoute>, ICloneable<PubSubRoute> {
|
||||
[JsonPropertyName("server_address")]
|
||||
public string? ServerAddress { get; set; }
|
||||
|
||||
public PubSubRoute(string the_server_address, string the_tag_reg_exp) {
|
||||
server_address = the_server_address;
|
||||
tag_reg_exp = the_tag_reg_exp;
|
||||
}
|
||||
[JsonPropertyName("tag")]
|
||||
public string? TagRegExp { get; set; }
|
||||
//route names are used in intermediary code (i.e. while substations are being deployed)
|
||||
//so that MC Hammer can advertise them.
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
public string[] getAllPubSubs() {
|
||||
return server_address.Split(';');
|
||||
}
|
||||
static readonly IComparer<PubSubRoute> comparer = new ComparerBuilder<PubSubRoute>() {
|
||||
p => p.ServerAddress,
|
||||
p => p.TagRegExp,
|
||||
};
|
||||
|
||||
public Regex get_regex() {
|
||||
return _regex ?? new Regex(tag_reg_exp);
|
||||
}
|
||||
Regex? myRegex;
|
||||
|
||||
#region IComparable<PubSubRoute> Members
|
||||
public PubSubRoute() { }
|
||||
|
||||
public int CompareTo(PubSubRoute other) {
|
||||
return server_address == other.server_address ? tag_reg_exp.CompareTo(other.tag_reg_exp)
|
||||
: server_address.CompareTo(other.server_address);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public override bool Equals(object obj) {
|
||||
if (!(obj is PubSubRoute) || obj == null)
|
||||
return false;
|
||||
else {
|
||||
PubSubRoute other = obj as PubSubRoute;
|
||||
return server_address.Equals(other.server_address) &&
|
||||
tag_reg_exp.Equals(other.tag_reg_exp);
|
||||
}
|
||||
}
|
||||
|
||||
public override int GetHashCode() {
|
||||
return base.GetHashCode();
|
||||
}
|
||||
|
||||
public static PubSubRoute get_default_route() {
|
||||
return new PubSubRoute("bluto://127.0.0.1:12345", ".*");
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return $"tag_reg_exp: {tag_reg_exp} server_address: {server_address}";
|
||||
}
|
||||
public PubSubRoute(string theServerAddress, string theTagRegExp) {
|
||||
ServerAddress = theServerAddress;
|
||||
TagRegExp = theTagRegExp;
|
||||
}
|
||||
|
||||
public static class PubSubRouteExtensions {
|
||||
public static bool IsMatch(this PubSubRoute route, string tag) =>
|
||||
route.get_regex().IsMatch(tag);
|
||||
[MemberNotNull(nameof(TagRegExp), nameof(myRegex))]
|
||||
public bool IsMatch(string tag) {
|
||||
if (TagRegExp is null) {
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
myRegex ??= new Regex(TagRegExp);
|
||||
return myRegex.IsMatch(tag);
|
||||
}
|
||||
|
||||
public class PubSubRouteJsonConverter : JsonConverter<PubSubRoute> {
|
||||
public override PubSubRoute? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
|
||||
var json_obj = JsonSerializer.Deserialize<JsonObject>(ref reader);
|
||||
if (json_obj is null) {
|
||||
return null;
|
||||
}
|
||||
var rv = new PubSubRoute();
|
||||
ReadString("name", ref rv.name);
|
||||
ReadString("server_address", ref rv.server_address);
|
||||
ReadString("tag", ref rv.tag_reg_exp);
|
||||
return rv;
|
||||
public PubSubRoute Clone() =>
|
||||
new() {
|
||||
ServerAddress = ServerAddress,
|
||||
TagRegExp = TagRegExp,
|
||||
Name = Name,
|
||||
};
|
||||
|
||||
void ReadString(string key, ref string valueOut) {
|
||||
if (json_obj.TryGetPropertyValue(key, out var valueJson)) {
|
||||
var valueStr = valueJson?.GetValue<string>();
|
||||
// Only (over)write valueOut if non-null
|
||||
if (valueStr is not null) {
|
||||
valueOut = valueStr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public int CompareTo(PubSubRoute? other) =>
|
||||
other is null ? -1 : comparer.Compare(this, other);
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, PubSubRoute value, JsonSerializerOptions options) {
|
||||
var jsonObj = new JsonObject {
|
||||
["name"] = value.name,
|
||||
["server_address"] = value.server_address,
|
||||
["tag"] = value.tag_reg_exp,
|
||||
};
|
||||
JsonSerializer.Serialize(writer, jsonObj);
|
||||
}
|
||||
}
|
||||
public bool Equals(PubSubRoute? other) =>
|
||||
other is not null
|
||||
&& ServerAddress == other.ServerAddress
|
||||
&& TagRegExp == other.TagRegExp;
|
||||
|
||||
public override bool Equals(object? obj) =>
|
||||
Equals(obj as PubSubRoute);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
HashCode.Combine(ServerAddress, TagRegExp);
|
||||
}
|
||||
Loaded 100 of 231 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user