diff --git a/Directory.Packages.props b/Directory.Packages.props
index 973f82f0b..803e5974c 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -38,6 +38,7 @@
+
diff --git a/samples/ChatApp/ChatApp.Unity/Assets/Scenes/ChatScene.unity b/samples/ChatApp/ChatApp.Unity/Assets/Scenes/ChatScene.unity
index 46292a812..72b1bc3c0 100644
--- a/samples/ChatApp/ChatApp.Unity/Assets/Scenes/ChatScene.unity
+++ b/samples/ChatApp/ChatApp.Unity/Assets/Scenes/ChatScene.unity
@@ -43,7 +43,7 @@ RenderSettings:
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
- serializedVersion: 11
+ serializedVersion: 12
m_GIWorkflowMode: 0
m_GISettings:
serializedVersion: 2
@@ -54,7 +54,7 @@ LightmapSettings:
m_EnableBakedLightmaps: 1
m_EnableRealtimeLightmaps: 1
m_LightmapEditorSettings:
- serializedVersion: 10
+ serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 40
m_AtlasSize: 1024
@@ -62,6 +62,7 @@ LightmapSettings:
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
+ m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
@@ -76,10 +77,16 @@ LightmapSettings:
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 500
m_PVRBounces: 2
+ m_PVREnvironmentSampleCount: 500
+ m_PVREnvironmentReferencePointCount: 2048
+ m_PVRFilteringMode: 2
+ m_PVRDenoiserTypeDirect: 0
+ m_PVRDenoiserTypeIndirect: 0
+ m_PVRDenoiserTypeAO: 0
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
- m_PVRFilteringMode: 2
+ m_PVREnvironmentMIS: 0
m_PVRCulling: 1
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 5
@@ -87,9 +94,11 @@ LightmapSettings:
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
- m_ShowResolutionOverlay: 1
+ m_ExportTrainingData: 0
+ m_TrainingDataDestination: TrainingData
+ m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 0}
- m_UseShadowmask: 1
+ m_LightingSettings: {fileID: 988913869}
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
@@ -109,6 +118,8 @@ NavMeshSettings:
manualTileSize: 0
tileSize: 256
accuratePlacement: 0
+ maxJobWorkers: 0
+ preserveTilesOutsideBounds: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
@@ -140,6 +151,7 @@ RectTransform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1634534318}
m_RootOrder: 0
@@ -158,17 +170,17 @@ MonoBehaviour:
m_GameObject: {fileID: 164639239}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.38679248, g: 0.38679248, b: 0.38679248, a: 1}
m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI,
- Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
m_Sprite: {fileID: 0}
m_Type: 1
m_PreserveAspect: 0
@@ -178,6 +190,7 @@ MonoBehaviour:
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 1
--- !u!222 &164639242
CanvasRenderer:
m_ObjectHideFlags: 0
@@ -215,6 +228,7 @@ RectTransform:
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 525475477}
m_Father: {fileID: 1309622324}
@@ -234,11 +248,12 @@ MonoBehaviour:
m_GameObject: {fileID: 190778106}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 1392445389, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
+ m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
@@ -248,17 +263,20 @@ MonoBehaviour:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
+ m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.11764706, g: 0.11764706, b: 0.11764706, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
+ m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
+ m_SelectedTrigger: Highlighted
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 190778109}
@@ -266,6 +284,7 @@ MonoBehaviour:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 1634534319}
+ m_TargetAssemblyTypeName:
m_MethodName: SendReport
m_Mode: 1
m_Arguments:
@@ -276,8 +295,6 @@ MonoBehaviour:
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
- m_TypeName: UnityEngine.UI.Button+ButtonClickedEvent, UnityEngine.UI, Version=1.0.0.0,
- Culture=neutral, PublicKeyToken=null
--- !u!114 &190778109
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -287,17 +304,17 @@ MonoBehaviour:
m_GameObject: {fileID: 190778106}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.5943396, g: 0.5747152, b: 0.5747152, a: 1}
m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI,
- Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
m_Sprite: {fileID: 0}
m_Type: 1
m_PreserveAspect: 0
@@ -307,6 +324,7 @@ MonoBehaviour:
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 1
--- !u!222 &190778110
CanvasRenderer:
m_ObjectHideFlags: 0
@@ -353,9 +371,10 @@ Camera:
m_ClearFlags: 1
m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
m_projectionMatrixMode: 1
+ m_GateFitMode: 2
+ m_FOVAxisMode: 0
m_SensorSize: {x: 36, y: 24}
m_LensShift: {x: 0, y: 0}
- m_GateFitMode: 2
m_FocalLength: 50
m_NormalizedViewPortRect:
serializedVersion: 2
@@ -393,6 +412,7 @@ Transform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 1, z: -10}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 2
@@ -425,6 +445,7 @@ RectTransform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1634534318}
m_RootOrder: 5
@@ -443,17 +464,17 @@ MonoBehaviour:
m_GameObject: {fileID: 247055287}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0, g: 0, b: 0, a: 1}
m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI,
- Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
m_Sprite: {fileID: 0}
m_Type: 0
m_PreserveAspect: 0
@@ -463,6 +484,7 @@ MonoBehaviour:
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 1
--- !u!222 &247055290
CanvasRenderer:
m_ObjectHideFlags: 0
@@ -500,6 +522,7 @@ RectTransform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1337382646}
- {fileID: 1329709669}
@@ -520,11 +543,12 @@ MonoBehaviour:
m_GameObject: {fileID: 330057072}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 575553740, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: d199490a83bb2b844b9695cbf13b01ef, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
+ m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
@@ -534,17 +558,20 @@ MonoBehaviour:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
+ m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
+ m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
+ m_SelectedTrigger: Highlighted
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 330057075}
@@ -558,16 +585,15 @@ MonoBehaviour:
m_HideMobileInput: 0
m_CharacterValidation: 0
m_CharacterLimit: 0
- m_OnEndEdit:
+ m_OnSubmit:
+ m_PersistentCalls:
+ m_Calls: []
+ m_OnDidEndEdit:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.InputField+SubmitEvent, UnityEngine.UI, Version=1.0.0.0,
- Culture=neutral, PublicKeyToken=null
m_OnValueChanged:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.InputField+OnChangeEvent, UnityEngine.UI, Version=1.0.0.0,
- Culture=neutral, PublicKeyToken=null
m_CaretColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_CustomCaretColor: 0
m_SelectionColor: {r: 0.65882355, g: 0.80784315, b: 1, a: 0.7529412}
@@ -575,6 +601,7 @@ MonoBehaviour:
m_CaretBlinkRate: 0.85
m_CaretWidth: 1
m_ReadOnly: 0
+ m_ShouldActivateOnSelect: 1
--- !u!114 &330057075
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -584,17 +611,17 @@ MonoBehaviour:
m_GameObject: {fileID: 330057072}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI,
- Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
m_Sprite: {fileID: 10911, guid: 0000000000000000f000000000000000, type: 0}
m_Type: 1
m_PreserveAspect: 0
@@ -604,6 +631,7 @@ MonoBehaviour:
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 1
--- !u!222 &330057076
CanvasRenderer:
m_ObjectHideFlags: 0
@@ -641,6 +669,7 @@ RectTransform:
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 610550729}
m_Father: {fileID: 632675412}
@@ -660,11 +689,12 @@ MonoBehaviour:
m_GameObject: {fileID: 390611515}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 1392445389, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
+ m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
@@ -674,17 +704,20 @@ MonoBehaviour:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
+ m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.11764706, g: 0.11764706, b: 0.11764706, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
+ m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
+ m_SelectedTrigger: Highlighted
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 390611518}
@@ -692,6 +725,7 @@ MonoBehaviour:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 1634534319}
+ m_TargetAssemblyTypeName:
m_MethodName: ReconnectInitializedServer
m_Mode: 1
m_Arguments:
@@ -702,8 +736,6 @@ MonoBehaviour:
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
- m_TypeName: UnityEngine.UI.Button+ButtonClickedEvent, UnityEngine.UI, Version=1.0.0.0,
- Culture=neutral, PublicKeyToken=null
--- !u!114 &390611518
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -713,17 +745,17 @@ MonoBehaviour:
m_GameObject: {fileID: 390611515}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.69753474, g: 0.70003784, b: 0.7075472, a: 1}
m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI,
- Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
m_Sprite: {fileID: 0}
m_Type: 1
m_PreserveAspect: 0
@@ -733,6 +765,7 @@ MonoBehaviour:
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 1
--- !u!222 &390611519
CanvasRenderer:
m_ObjectHideFlags: 0
@@ -769,6 +802,7 @@ RectTransform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 190778107}
m_RootOrder: 0
@@ -787,17 +821,17 @@ MonoBehaviour:
m_GameObject: {fileID: 525475476}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0, g: 0, b: 0, a: 1}
m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI,
- Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
m_FontData:
m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0}
m_FontSize: 14
@@ -848,6 +882,7 @@ RectTransform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1487425764}
- {fileID: 1383832575}
@@ -868,7 +903,7 @@ MonoBehaviour:
m_GameObject: {fileID: 557907330}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: -405508275, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Padding:
@@ -882,6 +917,9 @@ MonoBehaviour:
m_ChildForceExpandHeight: 1
m_ChildControlWidth: 1
m_ChildControlHeight: 1
+ m_ChildScaleWidth: 0
+ m_ChildScaleHeight: 0
+ m_ReverseArrangement: 0
--- !u!222 &557907334
CanvasRenderer:
m_ObjectHideFlags: 0
@@ -918,6 +956,7 @@ RectTransform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 10}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1096149280}
m_RootOrder: 0
@@ -936,17 +975,17 @@ MonoBehaviour:
m_GameObject: {fileID: 574419691}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0, g: 0, b: 0, a: 1}
m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI,
- Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
m_FontData:
m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0}
m_FontSize: 14
@@ -997,6 +1036,7 @@ RectTransform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1671399962}
m_RootOrder: 0
@@ -1015,17 +1055,17 @@ MonoBehaviour:
m_GameObject: {fileID: 609138041}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 0.5}
m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI,
- Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
m_FontData:
m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0}
m_FontSize: 30
@@ -1076,6 +1116,7 @@ RectTransform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 10}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 390611516}
m_RootOrder: 0
@@ -1094,17 +1135,17 @@ MonoBehaviour:
m_GameObject: {fileID: 610550728}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0, g: 0, b: 0, a: 1}
m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI,
- Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
m_FontData:
m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0}
m_FontSize: 14
@@ -1155,6 +1196,7 @@ RectTransform:
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 390611516}
- {fileID: 1096149280}
@@ -1185,7 +1227,7 @@ MonoBehaviour:
m_GameObject: {fileID: 632675411}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: -405508275, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Padding:
@@ -1199,6 +1241,9 @@ MonoBehaviour:
m_ChildForceExpandHeight: 1
m_ChildControlWidth: 1
m_ChildControlHeight: 1
+ m_ChildScaleWidth: 0
+ m_ChildScaleHeight: 0
+ m_ReverseArrangement: 0
--- !u!1 &771375552
GameObject:
m_ObjectHideFlags: 0
@@ -1227,6 +1272,7 @@ RectTransform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 10}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1487425764}
m_RootOrder: 0
@@ -1245,17 +1291,17 @@ MonoBehaviour:
m_GameObject: {fileID: 771375552}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0, g: 0, b: 0, a: 1}
m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI,
- Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
m_FontData:
m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0}
m_FontSize: 14
@@ -1306,6 +1352,7 @@ RectTransform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 963732563}
m_RootOrder: 0
@@ -1324,17 +1371,17 @@ MonoBehaviour:
m_GameObject: {fileID: 817501237}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI,
- Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
m_Type: 1
m_PreserveAspect: 0
@@ -1344,6 +1391,7 @@ MonoBehaviour:
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 1
--- !u!222 &817501240
CanvasRenderer:
m_ObjectHideFlags: 0
@@ -1381,6 +1429,7 @@ RectTransform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1978894556}
- {fileID: 1799772182}
@@ -1401,17 +1450,17 @@ MonoBehaviour:
m_GameObject: {fileID: 927062024}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI,
- Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
m_Sprite: {fileID: 0}
m_Type: 0
m_PreserveAspect: 0
@@ -1421,6 +1470,7 @@ MonoBehaviour:
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 1
--- !u!222 &927062027
CanvasRenderer:
m_ObjectHideFlags: 0
@@ -1438,7 +1488,7 @@ MonoBehaviour:
m_GameObject: {fileID: 927062024}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 1367256648, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: 1aa08ab6e0800fa44ae55d278d1423e3, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Content: {fileID: 1522127878}
@@ -1459,8 +1509,86 @@ MonoBehaviour:
m_OnValueChanged:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.ScrollRect+ScrollRectEvent, UnityEngine.UI, Version=1.0.0.0,
- Culture=neutral, PublicKeyToken=null
+--- !u!1 &934640857
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 934640860}
+ - component: {fileID: 934640859}
+ - component: {fileID: 934640858}
+ m_Layer: 5
+ m_Name: LabelRtt
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &934640858
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 934640857}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_Material: {fileID: 0}
+ m_Color: {r: 0.8, g: 0.8, b: 0.8, a: 1}
+ m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
+ m_OnCullStateChanged:
+ m_PersistentCalls:
+ m_Calls: []
+ m_FontData:
+ m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0}
+ m_FontSize: 12
+ m_FontStyle: 0
+ m_BestFit: 0
+ m_MinSize: 1
+ m_MaxSize: 40
+ m_Alignment: 5
+ m_AlignByGeometry: 0
+ m_RichText: 1
+ m_HorizontalOverflow: 0
+ m_VerticalOverflow: 0
+ m_LineSpacing: 1
+ m_Text: 'RTT: -ms'
+--- !u!222 &934640859
+CanvasRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 934640857}
+ m_CullTransparentMesh: 1
+--- !u!224 &934640860
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 934640857}
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 1634534318}
+ m_RootOrder: 7
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 1, y: 1}
+ m_AnchorMax: {x: 1, y: 1}
+ m_AnchoredPosition: {x: -10, y: -10}
+ m_SizeDelta: {x: 79.29999, y: 30}
+ m_Pivot: {x: 1, y: 1}
--- !u!1 &963732562
GameObject:
m_ObjectHideFlags: 0
@@ -1487,6 +1615,7 @@ RectTransform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 817501238}
m_Father: {fileID: 1799772182}
@@ -1497,6 +1626,68 @@ RectTransform:
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: -20, y: -20}
m_Pivot: {x: 0.5, y: 0.5}
+--- !u!850595691 &988913869
+LightingSettings:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_Name: Settings.lighting
+ serializedVersion: 4
+ m_GIWorkflowMode: 0
+ m_EnableBakedLightmaps: 1
+ m_EnableRealtimeLightmaps: 1
+ m_RealtimeEnvironmentLighting: 1
+ m_BounceScale: 1
+ m_AlbedoBoost: 1
+ m_IndirectOutputScale: 1
+ m_UsingShadowmask: 1
+ m_BakeBackend: 1
+ m_LightmapMaxSize: 1024
+ m_BakeResolution: 40
+ m_Padding: 2
+ m_LightmapCompression: 3
+ m_AO: 0
+ m_AOMaxDistance: 1
+ m_CompAOExponent: 1
+ m_CompAOExponentDirect: 0
+ m_ExtractAO: 0
+ m_MixedBakeMode: 2
+ m_LightmapsBakeMode: 1
+ m_FilterMode: 1
+ m_LightmapParameters: {fileID: 15204, guid: 0000000000000000f000000000000000, type: 0}
+ m_ExportTrainingData: 0
+ m_TrainingDataDestination: TrainingData
+ m_RealtimeResolution: 2
+ m_ForceWhiteAlbedo: 0
+ m_ForceUpdates: 0
+ m_FinalGather: 0
+ m_FinalGatherRayCount: 256
+ m_FinalGatherFiltering: 1
+ m_PVRCulling: 1
+ m_PVRSampling: 1
+ m_PVRDirectSampleCount: 32
+ m_PVRSampleCount: 500
+ m_PVREnvironmentSampleCount: 500
+ m_PVREnvironmentReferencePointCount: 2048
+ m_LightProbeSampleCountMultiplier: 4
+ m_PVRBounces: 2
+ m_PVRMinBounces: 2
+ m_PVREnvironmentMIS: 0
+ m_PVRFilteringMode: 2
+ m_PVRDenoiserTypeDirect: 0
+ m_PVRDenoiserTypeIndirect: 0
+ m_PVRDenoiserTypeAO: 0
+ m_PVRFilterTypeDirect: 0
+ m_PVRFilterTypeIndirect: 0
+ m_PVRFilterTypeAO: 0
+ m_PVRFilteringGaussRadiusDirect: 1
+ m_PVRFilteringGaussRadiusIndirect: 5
+ m_PVRFilteringGaussRadiusAO: 2
+ m_PVRFilteringAtrousPositionSigmaDirect: 0.5
+ m_PVRFilteringAtrousPositionSigmaIndirect: 2
+ m_PVRFilteringAtrousPositionSigmaAO: 1
+ m_PVRTiledBaking: 0
--- !u!1 &1040922706
GameObject:
m_ObjectHideFlags: 0
@@ -1525,6 +1716,7 @@ RectTransform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1868729446}
m_RootOrder: 0
@@ -1543,17 +1735,17 @@ MonoBehaviour:
m_GameObject: {fileID: 1040922706}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0, g: 0, b: 0, a: 1}
m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI,
- Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
m_FontData:
m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0}
m_FontSize: 14
@@ -1605,6 +1797,7 @@ RectTransform:
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 574419692}
m_Father: {fileID: 632675412}
@@ -1624,11 +1817,12 @@ MonoBehaviour:
m_GameObject: {fileID: 1096149279}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 1392445389, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
+ m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
@@ -1638,17 +1832,20 @@ MonoBehaviour:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
+ m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.11764706, g: 0.11764706, b: 0.11764706, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
+ m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
+ m_SelectedTrigger: Highlighted
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 1096149282}
@@ -1656,6 +1853,7 @@ MonoBehaviour:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 1634534319}
+ m_TargetAssemblyTypeName:
m_MethodName: DisconnectServer
m_Mode: 1
m_Arguments:
@@ -1666,8 +1864,6 @@ MonoBehaviour:
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
- m_TypeName: UnityEngine.UI.Button+ButtonClickedEvent, UnityEngine.UI, Version=1.0.0.0,
- Culture=neutral, PublicKeyToken=null
--- !u!114 &1096149282
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -1677,17 +1873,17 @@ MonoBehaviour:
m_GameObject: {fileID: 1096149279}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.69753474, g: 0.70003784, b: 0.7075472, a: 1}
m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI,
- Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
m_Sprite: {fileID: 0}
m_Type: 1
m_PreserveAspect: 0
@@ -1697,6 +1893,7 @@ MonoBehaviour:
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 1
--- !u!222 &1096149283
CanvasRenderer:
m_ObjectHideFlags: 0
@@ -1733,6 +1930,7 @@ RectTransform:
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1671399962}
- {fileID: 190778107}
@@ -1761,7 +1959,7 @@ MonoBehaviour:
m_GameObject: {fileID: 1309622323}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: -405508275, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Padding:
@@ -1775,6 +1973,9 @@ MonoBehaviour:
m_ChildForceExpandHeight: 1
m_ChildControlWidth: 1
m_ChildControlHeight: 1
+ m_ChildScaleWidth: 0
+ m_ChildScaleHeight: 0
+ m_ReverseArrangement: 0
--- !u!1 &1329709668
GameObject:
m_ObjectHideFlags: 0
@@ -1803,6 +2004,7 @@ RectTransform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 330057073}
m_RootOrder: 1
@@ -1821,17 +2023,17 @@ MonoBehaviour:
m_GameObject: {fileID: 1329709668}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI,
- Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
m_FontData:
m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0}
m_FontSize: 30
@@ -1882,6 +2084,7 @@ RectTransform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 330057073}
m_RootOrder: 0
@@ -1900,17 +2103,17 @@ MonoBehaviour:
m_GameObject: {fileID: 1337382645}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 0.5}
m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI,
- Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
m_FontData:
m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0}
m_FontSize: 30
@@ -1962,6 +2165,7 @@ RectTransform:
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1420340486}
m_Father: {fileID: 557907331}
@@ -1981,11 +2185,12 @@ MonoBehaviour:
m_GameObject: {fileID: 1383832574}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 1392445389, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
+ m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
@@ -1995,17 +2200,20 @@ MonoBehaviour:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
+ m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.11764706, g: 0.11764706, b: 0.11764706, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
+ m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
+ m_SelectedTrigger: Highlighted
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 1383832577}
@@ -2013,6 +2221,7 @@ MonoBehaviour:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 1634534319}
+ m_TargetAssemblyTypeName:
m_MethodName: SendMessage
m_Mode: 1
m_Arguments:
@@ -2023,8 +2232,6 @@ MonoBehaviour:
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
- m_TypeName: UnityEngine.UI.Button+ButtonClickedEvent, UnityEngine.UI, Version=1.0.0.0,
- Culture=neutral, PublicKeyToken=null
--- !u!114 &1383832577
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -2034,17 +2241,17 @@ MonoBehaviour:
m_GameObject: {fileID: 1383832574}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.33521715, g: 0.764151, b: 0.5044038, a: 1}
m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI,
- Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
m_Sprite: {fileID: 0}
m_Type: 1
m_PreserveAspect: 0
@@ -2054,6 +2261,7 @@ MonoBehaviour:
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 1
--- !u!222 &1383832578
CanvasRenderer:
m_ObjectHideFlags: 0
@@ -2089,9 +2297,10 @@ MonoBehaviour:
m_GameObject: {fileID: 1409526054}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 1077351063, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3}
m_Name:
m_EditorClassIdentifier:
+ m_SendPointerHoverToParent: 1
m_HorizontalAxis: Horizontal
m_VerticalAxis: Vertical
m_SubmitButton: Submit
@@ -2108,7 +2317,7 @@ MonoBehaviour:
m_GameObject: {fileID: 1409526054}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: -619905303, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3}
m_Name:
m_EditorClassIdentifier:
m_FirstSelected: {fileID: 0}
@@ -2124,6 +2333,7 @@ Transform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 1
@@ -2156,6 +2366,7 @@ RectTransform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1383832575}
m_RootOrder: 0
@@ -2174,17 +2385,17 @@ MonoBehaviour:
m_GameObject: {fileID: 1420340485}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0, g: 0, b: 0, a: 1}
m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI,
- Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
m_FontData:
m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0}
m_FontSize: 14
@@ -2235,6 +2446,7 @@ RectTransform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 2120537442}
m_RootOrder: 0
@@ -2253,17 +2465,17 @@ MonoBehaviour:
m_GameObject: {fileID: 1434588224}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0, g: 0, b: 0, a: 1}
m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI,
- Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
m_FontData:
m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0}
m_FontSize: 14
@@ -2315,6 +2527,7 @@ RectTransform:
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 771375553}
m_Father: {fileID: 557907331}
@@ -2334,11 +2547,12 @@ MonoBehaviour:
m_GameObject: {fileID: 1487425763}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 1392445389, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
+ m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
@@ -2348,17 +2562,20 @@ MonoBehaviour:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
+ m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.11764706, g: 0.11764706, b: 0.11764706, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
+ m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
+ m_SelectedTrigger: Highlighted
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 1487425766}
@@ -2366,6 +2583,7 @@ MonoBehaviour:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 1634534319}
+ m_TargetAssemblyTypeName:
m_MethodName: JoinOrLeave
m_Mode: 1
m_Arguments:
@@ -2376,8 +2594,6 @@ MonoBehaviour:
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
- m_TypeName: UnityEngine.UI.Button+ButtonClickedEvent, UnityEngine.UI, Version=1.0.0.0,
- Culture=neutral, PublicKeyToken=null
--- !u!114 &1487425766
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -2387,17 +2603,17 @@ MonoBehaviour:
m_GameObject: {fileID: 1487425763}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.3137255, g: 0.5686275, b: 0.627451, a: 1}
m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI,
- Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
m_Sprite: {fileID: 0}
m_Type: 1
m_PreserveAspect: 0
@@ -2407,6 +2623,7 @@ MonoBehaviour:
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 1
--- !u!222 &1487425767
CanvasRenderer:
m_ObjectHideFlags: 0
@@ -2444,13 +2661,14 @@ RectTransform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1978894556}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.02, y: 0.02}
m_AnchorMax: {x: 0.98, y: 0.98}
- m_AnchoredPosition: {x: 0, y: 7.8887787}
+ m_AnchoredPosition: {x: 0, y: 6.5812073}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 1}
--- !u!114 &1522127879
@@ -2462,7 +2680,7 @@ MonoBehaviour:
m_GameObject: {fileID: 1522127877}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 1741964061, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3}
m_Name:
m_EditorClassIdentifier:
m_HorizontalFit: 0
@@ -2476,17 +2694,17 @@ MonoBehaviour:
m_GameObject: {fileID: 1522127877}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI,
- Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
m_FontData:
m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0}
m_FontSize: 14
@@ -2538,7 +2756,7 @@ MonoBehaviour:
m_GameObject: {fileID: 1634534314}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 1301386320, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3}
m_Name:
m_EditorClassIdentifier:
m_IgnoreReversedGraphics: 1
@@ -2555,7 +2773,7 @@ MonoBehaviour:
m_GameObject: {fileID: 1634534314}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 1980459831, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3}
m_Name:
m_EditorClassIdentifier:
m_UiScaleMode: 1
@@ -2568,6 +2786,7 @@ MonoBehaviour:
m_FallbackScreenDPI: 96
m_DefaultSpriteDPI: 96
m_DynamicPixelsPerUnit: 1
+ m_PresetInfoIsWorld: 0
--- !u!223 &1634534317
Canvas:
m_ObjectHideFlags: 0
@@ -2599,6 +2818,7 @@ RectTransform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 0, y: 0, z: 0}
+ m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 164639240}
- {fileID: 927062025}
@@ -2607,6 +2827,7 @@ RectTransform:
- {fileID: 632675412}
- {fileID: 247055288}
- {fileID: 1309622324}
+ - {fileID: 934640860}
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
@@ -2637,6 +2858,7 @@ MonoBehaviour:
DisconnectButon: {fileID: 1096149281}
ExceptionButton: {fileID: 1868729447}
UnaryExceptionButton: {fileID: 2120537443}
+ LabelRtt: {fileID: 934640858}
--- !u!1 &1671399961
GameObject:
m_ObjectHideFlags: 0
@@ -2666,6 +2888,7 @@ RectTransform:
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 609138042}
- {fileID: 1901126666}
@@ -2686,11 +2909,12 @@ MonoBehaviour:
m_GameObject: {fileID: 1671399961}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 575553740, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: d199490a83bb2b844b9695cbf13b01ef, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
+ m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
@@ -2700,17 +2924,20 @@ MonoBehaviour:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
+ m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
+ m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
+ m_SelectedTrigger: Highlighted
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 1671399964}
@@ -2724,16 +2951,15 @@ MonoBehaviour:
m_HideMobileInput: 0
m_CharacterValidation: 0
m_CharacterLimit: 0
- m_OnEndEdit:
+ m_OnSubmit:
+ m_PersistentCalls:
+ m_Calls: []
+ m_OnDidEndEdit:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.InputField+SubmitEvent, UnityEngine.UI, Version=1.0.0.0,
- Culture=neutral, PublicKeyToken=null
m_OnValueChanged:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.InputField+OnChangeEvent, UnityEngine.UI, Version=1.0.0.0,
- Culture=neutral, PublicKeyToken=null
m_CaretColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_CustomCaretColor: 0
m_SelectionColor: {r: 0.65882355, g: 0.80784315, b: 1, a: 0.7529412}
@@ -2741,6 +2967,7 @@ MonoBehaviour:
m_CaretBlinkRate: 0.85
m_CaretWidth: 1
m_ReadOnly: 0
+ m_ShouldActivateOnSelect: 1
--- !u!114 &1671399964
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -2750,17 +2977,17 @@ MonoBehaviour:
m_GameObject: {fileID: 1671399961}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI,
- Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
m_Sprite: {fileID: 10911, guid: 0000000000000000f000000000000000, type: 0}
m_Type: 1
m_PreserveAspect: 0
@@ -2770,6 +2997,7 @@ MonoBehaviour:
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 1
--- !u!222 &1671399965
CanvasRenderer:
m_ObjectHideFlags: 0
@@ -2807,6 +3035,7 @@ RectTransform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 963732563}
m_Father: {fileID: 927062025}
@@ -2826,11 +3055,12 @@ MonoBehaviour:
m_GameObject: {fileID: 1799772181}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: -2061169968, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: 2a4db7a114972834c8e4117be1d82ba3, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
+ m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
@@ -2840,30 +3070,31 @@ MonoBehaviour:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
+ m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
+ m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
+ m_SelectedTrigger: Highlighted
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 817501239}
m_HandleRect: {fileID: 817501238}
m_Direction: 3
m_Value: 0
- m_Size: 0.99999994
+ m_Size: 1
m_NumberOfSteps: 0
m_OnValueChanged:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.Scrollbar+ScrollEvent, UnityEngine.UI, Version=1.0.0.0,
- Culture=neutral, PublicKeyToken=null
--- !u!114 &1799772184
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -2873,17 +3104,17 @@ MonoBehaviour:
m_GameObject: {fileID: 1799772181}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI,
- Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
m_Sprite: {fileID: 10907, guid: 0000000000000000f000000000000000, type: 0}
m_Type: 1
m_PreserveAspect: 0
@@ -2893,6 +3124,7 @@ MonoBehaviour:
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 1
--- !u!222 &1799772185
CanvasRenderer:
m_ObjectHideFlags: 0
@@ -2930,6 +3162,7 @@ RectTransform:
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1040922707}
m_Father: {fileID: 632675412}
@@ -2949,11 +3182,12 @@ MonoBehaviour:
m_GameObject: {fileID: 1868729445}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 1392445389, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
+ m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
@@ -2963,17 +3197,20 @@ MonoBehaviour:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
+ m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.11764706, g: 0.11764706, b: 0.11764706, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
+ m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
+ m_SelectedTrigger: Highlighted
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 1868729448}
@@ -2981,6 +3218,7 @@ MonoBehaviour:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 1634534319}
+ m_TargetAssemblyTypeName:
m_MethodName: GenerateException
m_Mode: 1
m_Arguments:
@@ -2991,8 +3229,6 @@ MonoBehaviour:
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
- m_TypeName: UnityEngine.UI.Button+ButtonClickedEvent, UnityEngine.UI, Version=1.0.0.0,
- Culture=neutral, PublicKeyToken=null
--- !u!114 &1868729448
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -3002,17 +3238,17 @@ MonoBehaviour:
m_GameObject: {fileID: 1868729445}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.7647059, g: 0.33333337, b: 0.3651321, a: 1}
m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI,
- Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
m_Sprite: {fileID: 0}
m_Type: 1
m_PreserveAspect: 0
@@ -3022,6 +3258,7 @@ MonoBehaviour:
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 1
--- !u!222 &1868729449
CanvasRenderer:
m_ObjectHideFlags: 0
@@ -3058,6 +3295,7 @@ RectTransform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1671399962}
m_RootOrder: 1
@@ -3076,17 +3314,17 @@ MonoBehaviour:
m_GameObject: {fileID: 1901126665}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI,
- Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
m_FontData:
m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0}
m_FontSize: 30
@@ -3137,6 +3375,7 @@ RectTransform:
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1522127878}
m_Father: {fileID: 927062025}
@@ -3164,9 +3403,11 @@ MonoBehaviour:
m_GameObject: {fileID: 1978894555}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: -146154839, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: 3312d7739989d2b4e91e6319e9a96d76, type: 3}
m_Name:
m_EditorClassIdentifier:
+ m_Padding: {x: 0, y: 0, z: 0, w: 0}
+ m_Softness: {x: 0, y: 0}
--- !u!1 &2120537441
GameObject:
m_ObjectHideFlags: 0
@@ -3196,6 +3437,7 @@ RectTransform:
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1434588225}
m_Father: {fileID: 632675412}
@@ -3215,11 +3457,12 @@ MonoBehaviour:
m_GameObject: {fileID: 2120537441}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 1392445389, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
+ m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
@@ -3229,17 +3472,20 @@ MonoBehaviour:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
+ m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.11764706, g: 0.11764706, b: 0.11764706, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
+ m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
+ m_SelectedTrigger: Highlighted
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 2120537444}
@@ -3247,6 +3493,7 @@ MonoBehaviour:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 1634534319}
+ m_TargetAssemblyTypeName:
m_MethodName: UnaryGenerateException
m_Mode: 1
m_Arguments:
@@ -3257,8 +3504,6 @@ MonoBehaviour:
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
- m_TypeName: UnityEngine.UI.Button+ButtonClickedEvent, UnityEngine.UI, Version=1.0.0.0,
- Culture=neutral, PublicKeyToken=null
--- !u!114 &2120537444
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -3268,17 +3513,17 @@ MonoBehaviour:
m_GameObject: {fileID: 2120537441}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: -765806418, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.7647059, g: 0.33333337, b: 0.3651321, a: 1}
m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI,
- Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
m_Sprite: {fileID: 0}
m_Type: 1
m_PreserveAspect: 0
@@ -3288,6 +3533,7 @@ MonoBehaviour:
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 1
--- !u!222 &2120537445
CanvasRenderer:
m_ObjectHideFlags: 0
diff --git a/samples/ChatApp/ChatApp.Unity/Assets/Scripts/ChatComponent.cs b/samples/ChatApp/ChatApp.Unity/Assets/Scripts/ChatComponent.cs
index 0839f01b1..8b0264bba 100644
--- a/samples/ChatApp/ChatApp.Unity/Assets/Scripts/ChatComponent.cs
+++ b/samples/ChatApp/ChatApp.Unity/Assets/Scripts/ChatComponent.cs
@@ -42,6 +42,7 @@ public class ChatComponent : MonoBehaviour, IChatHubReceiver
public Button ExceptionButton;
public Button UnaryExceptionButton;
+ public Text LabelRtt;
async void Start()
{
@@ -71,7 +72,11 @@ private async Task InitializeClientAsync()
try
{
Debug.Log($"Connecting to the server...");
- this.streamingClient = await StreamingHubClient.ConnectAsync(this.channel, this, cancellationToken: shutdownCancellation.Token);
+ var options = StreamingHubClientOptions.CreateWithDefault()
+ .WithClientHeartbeatResponseReceived(x => LabelRtt.text = $"RTT: {x.RoundTripTime.TotalMilliseconds:#,0}ms")
+ .WithClientHeartbeatInterval(TimeSpan.FromSeconds(10))
+ .WithClientHeartbeatTimeout(TimeSpan.FromSeconds(5));
+ this.streamingClient = await StreamingHubClient.ConnectAsync(this.channel, this, options, cancellationToken: shutdownCancellation.Token);
this.RegisterDisconnectEvent(streamingClient);
Debug.Log($"Connection is established.");
break;
diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubClientMessageReader.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubClientMessageReader.cs
index 247caec1f..5d4b59474 100644
--- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubClientMessageReader.cs
+++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubClientMessageReader.cs
@@ -24,8 +24,9 @@ public StreamingHubMessageType ReadMessageType()
4 => StreamingHubMessageType.ResponseWithError,
5 => reader.ReadByte() switch
{
- 0x00 /* 0:ClientResultRequest */ => StreamingHubMessageType.ClientResultRequest,
- 0x7f /* 127:Heartbeat */ => StreamingHubMessageType.Heartbeat,
+ 0x00 /* 0:ClientResultRequest */ => StreamingHubMessageType.ClientResultRequest,
+ 0x7e /* 126:ClientHeartbeatResponse */ => StreamingHubMessageType.ClientHeartbeatResponse,
+ 0x7f /* 127:ServerHeartbeat */ => StreamingHubMessageType.ServerHeartbeat,
var x => throw new InvalidOperationException($"Unknown Type: {x}"),
},
_ => throw new InvalidOperationException($"Unknown message format: ArrayLength = {arrayLength}"),
@@ -73,7 +74,7 @@ public StreamingHubMessageType ReadMessageType()
return (clientRequestMessageId, methodId, data.Slice(offset));
}
- public ReadOnlyMemory ReadHeartbeat()
+ public ReadOnlyMemory ReadServerHeartbeat()
{
//var type = reader.ReadByte(); // Type is already read by ReadMessageType
reader.Skip(); // Dummy (1)
@@ -82,5 +83,20 @@ public ReadOnlyMemory ReadHeartbeat()
return data.Slice((int)reader.Consumed);
}
+
+ public long ReadClientHeartbeatResponse()
+ {
+ //var type = reader.ReadByte(); // Type is already read by ReadMessageType
+ reader.Skip(); // Dummy (1)
+ reader.Skip(); // Dummy (2)
+ reader.Skip(); // Dummy (3)
+
+ // Extra: [SentAt(long)]
+ var arrayLen = reader.ReadArrayHeader();
+ if (arrayLen == 0) throw new InvalidOperationException("Invalid client heartbeat response. An extra data is empty.");
+ var sentAt = reader.ReadInt64();
+
+ return sentAt;
+ }
}
}
diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubMessageWriter.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubMessageWriter.cs
index 93832281e..9be226ff4 100644
--- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubMessageWriter.cs
+++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubMessageWriter.cs
@@ -26,9 +26,13 @@ namespace MagicOnion.Internal
/// Array(5): [Type=0x00, Nil, ClientResultMessageId(Guid), MethodId(int), SerializedArguments]
///
/// -
- /// Heartbeat:
+ /// ServerHeartbeat/Request:
/// Array(5): [Type=0x7f, Nil, Nil, Nil, Extras]
///
+ /// -
+ /// ClientHeartbeat/Response:
+ /// Array(5): [Type=0x7e, Nil, Nil, Nil, [ClientTime(long)]]
+ ///
///
/// StreamingHub message formats (from Client to Server):
///
@@ -49,9 +53,13 @@ namespace MagicOnion.Internal
/// Array(4): [Type=0x01, ClientResultMessageId(Guid), MethodId(int), [StatusCode(int), Detail(string), Message(string)]]
///
/// -
- /// Heartbeat/Response:
+ /// ServerHeartbeat/Response:
/// Array(4): [Type=0x7f, Nil, Nil, Nil]
///
+ /// -
+ /// ClientHeartbeat/Request:
+ /// Array(4): [Type=0x7e, Nil, Nil, [ClientTime(long)]]
+ ///
///
///
internal static class StreamingHubMessageWriter
@@ -193,58 +201,138 @@ public static void WriteClientResultResponseMessageForError(IBufferWriter
// Array(5)[127, Nil, Nil, Nil, ]
- static ReadOnlySpan HeartbeatMessageForServerToClientHeader => new byte[] { 0x95, 0x7f, 0xc0, 0xc0, 0xc0 };
+ static ReadOnlySpan ServerHeartbeatMessageForServerToClientHeader => new byte[] { 0x95, 0x7f, 0xc0, 0xc0, 0xc0 };
///
- /// Writes a heartbeat message for sending from the server.
+ /// Writes a server heartbeat message for sending from the server.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void WriteHeartbeatMessageForServerToClientHeader(IBufferWriter bufferWriter)
+ public static void WriteServerHeartbeatMessageHeader(IBufferWriter bufferWriter)
{
- bufferWriter.Write(HeartbeatMessageForServerToClientHeader);
+ bufferWriter.Write(ServerHeartbeatMessageForServerToClientHeader);
//var writer = new MessagePackWriter(bufferWriter);
//writer.WriteArrayHeader(5);
//writer.Write(0x7f); // Type = 0x7f / 127 (Heartbeat)
+ //writer.WriteNil(); // Dummy
+ //writer.WriteNil(); // Dummy
+ //writer.WriteNil(); // Dummy
+ //writer.Flush();
+ // //
+ }
+
+ // Array(4)[127, Nil, Nil, Nil]
+ static ReadOnlySpan ServerHeartbeatMessageForClientToServer => new byte[] { 0x94, 0x7f, 0xc0, 0xc0, 0xc0 };
+
+ ///
+ /// Writes a server heartbeat message for sending response from the client.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void WriteServerHeartbeatMessageResponse(IBufferWriter bufferWriter)
+ {
+ bufferWriter.Write(ServerHeartbeatMessageForClientToServer);
+ //var writer = new MessagePackWriter(bufferWriter);
+ //writer.WriteArrayHeader(4);
+ //writer.Write(0x7f); // Type = 0x7f / 127 (Heartbeat)
//writer.WriteNil(); // Dummy
//writer.WriteNil(); // Dummy
//writer.WriteNil(); // Dummy
//writer.Flush();
}
- // Array(4)[127, Nil, Nil, Nil]
- static ReadOnlySpan HeartbeatMessageForClientToServer => new byte[] { 0x94, 0x7f, 0xc0, 0xc0, 0xc0 };
+ // Array(4)[0x7e(126), Nil, Nil, ]
+ static ReadOnlySpan ClientHeartbeatMessageHeader => new byte[] { 0x94, 0x7e, 0xc0, 0xc0 };
///
- /// Writes a heartbeat message for sending from the client.
+ /// Writes a client heartbeat message for sending from the client.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void WriteHeartbeatMessageForClientToServer(IBufferWriter bufferWriter)
+ public static void WriteClientHeartbeatMessageHeader(IBufferWriter bufferWriter)
{
- bufferWriter.Write(HeartbeatMessageForClientToServer);
+ bufferWriter.Write(ClientHeartbeatMessageHeader);
//var writer = new MessagePackWriter(bufferWriter);
//writer.WriteArrayHeader(4);
- //writer.Write(0x7f); // Type = 0x7f / 127 (Heartbeat)
+ //writer.Write(0x7f); // Type = 0x7e / 126 (ClientHeartbeat)
+ //writer.WriteNil(); // Dummy
+ //writer.WriteNil(); // Dummy
+ //writer.Flush();
+ // //
+ }
+
+ // Array(5)[0x7e(126), Nil, Nil, Nil, ]
+ static ReadOnlySpan ClientHeartbeatMessageResponseHeader => new byte[] { 0x95, 0x7e, 0xc0, 0xc0, 0xc0 };
+
+ ///
+ /// Writes a client heartbeat message for sending response from the server.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void WriteClientHeartbeatMessageResponseHeader(IBufferWriter bufferWriter)
+ {
+ bufferWriter.Write(ClientHeartbeatMessageResponseHeader);
+ //var writer = new MessagePackWriter(bufferWriter);
+ //writer.WriteArrayHeader(5);
+ //writer.Write(0x7f); // Type = 0x7e / 126 (Heartbeat)
//writer.WriteNil(); // Dummy
//writer.WriteNil(); // Dummy
//writer.WriteNil(); // Dummy
//writer.Flush();
+ // //
}
}
internal enum StreamingHubMessageType
{
- // Client to Server
+ ///
+ /// Request: Client -> Server
+ ///
Request,
+ ///
+ /// Request: Client -> Server / Fire-and-Forget
+ ///
RequestFireAndForget,
+ ///
+ /// Request: Client -> Server -> Client
+ ///
Response,
+ ///
+ /// Request: Client -> Server -(Error)-> Client
+ ///
ResponseWithError,
- HeartbeatResponse,
- // Server to Client
+ ///
+ /// Broadcast: Server -> Client
+ ///
Broadcast,
+
+ ///
+ /// ClientResult: Server -> Client
+ ///
ClientResultRequest,
+ ///
+ /// ClientResult: Server -> Client -> Server
+ ///
ClientResultResponse,
+ ///
+ /// ClientResult: Server -> Client -(Error)-> Server
+ ///
ClientResultResponseWithError,
- Heartbeat,
+
+
+ ///
+ /// Heartbeat: Server -> Client -> Server
+ ///
+ ServerHeartbeatResponse,
+ ///
+ /// Heartbeat: Server -> Client
+ ///
+ ServerHeartbeat,
+
+ ///
+ /// Heartbeat: Client -> Server
+ ///
+ ClientHeartbeat,
+ ///
+ /// Heartbeat: Client -> Server -> Client
+ ///
+ ClientHeartbeatResponse,
}
}
diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayload.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayload.cs
index d3387100f..d4c5b1ee0 100644
--- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayload.cs
+++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayload.cs
@@ -104,12 +104,21 @@ public void Initialize(ReadOnlySequence data)
memory = buffer.AsMemory(0, (int)data.Length);
}
- public void Initialize(ReadOnlyMemory data)
+ public void Initialize(ReadOnlyMemory data, bool holdReference)
{
ThrowIfUsing();
- buffer = null;
- memory = data;
+ if (holdReference)
+ {
+ buffer = null;
+ memory = data;
+ }
+ else
+ {
+ buffer = ArrayPool.Shared.Rent((int)data.Length);
+ data.CopyTo(buffer);
+ memory = buffer.AsMemory(0, (int)data.Length);
+ }
}
public void Uninitialize()
diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayloadPool.BuiltIn.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayloadPool.BuiltIn.cs
index 8a8334abb..2245d3e88 100644
--- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayloadPool.BuiltIn.cs
+++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayloadPool.BuiltIn.cs
@@ -105,10 +105,10 @@ public StreamingHubPayload RentOrCreate(ReadOnlySpan data)
#endif
}
- public StreamingHubPayload RentOrCreate(ReadOnlyMemory data)
+ public StreamingHubPayload RentOrCreate(ReadOnlyMemory data, bool holdReference)
{
var payload = pool.RentOrCreateCore();
- payload.Initialize(data);
+ payload.Initialize(data, holdReference);
#if DEBUG
return new StreamingHubPayload(payload);
#else
diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayloadPool.ObjectPool.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayloadPool.ObjectPool.cs
index 62c85b124..1a0948825 100644
--- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayloadPool.ObjectPool.cs
+++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubPayloadPool.ObjectPool.cs
@@ -34,10 +34,10 @@ public StreamingHubPayload RentOrCreate(ReadOnlySpan data)
#endif
}
- public StreamingHubPayload RentOrCreate(ReadOnlyMemory data)
+ public StreamingHubPayload RentOrCreate(ReadOnlyMemory data, bool holdReference)
{
var payload = pool.Get();
- payload.Initialize(data);
+ payload.Initialize(data, holdReference);
#if DEBUG
return new StreamingHubPayload(payload);
#else
diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubServerMessageReader.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubServerMessageReader.cs
index b1d8326cf..ae03323b2 100644
--- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubServerMessageReader.cs
+++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/Internal.Shared/StreamingHubServerMessageReader.cs
@@ -25,7 +25,8 @@ public StreamingHubMessageType ReadMessageType()
{
0x00 => StreamingHubMessageType.ClientResultResponse,
0x01 => StreamingHubMessageType.ClientResultResponseWithError,
- 0x7f => StreamingHubMessageType.HeartbeatResponse,
+ 0x7e => StreamingHubMessageType.ClientHeartbeat,
+ 0x7f => StreamingHubMessageType.ServerHeartbeatResponse,
var subType => throw new InvalidOperationException($"Unknown client response message: {subType}"),
},
_ => throw new InvalidOperationException($"Unknown message format: ArrayLength = {arrayLength}"),
@@ -75,5 +76,14 @@ public StreamingHubMessageType ReadMessageType()
return (clientResultMessageId, clientMethodId, statusCode, detail, message);
}
+
+ public ReadOnlyMemory ReadClientHeartbeat()
+ {
+ // [Nil, Nil, [SentAt(long)]]
+ reader.Skip(); // Dummy
+ reader.Skip(); // Dummy
+
+ return data.Slice((int)reader.Consumed);
+ }
}
}
diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientBase.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientBase.cs
index 51905894f..613909c7a 100644
--- a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientBase.cs
+++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientBase.cs
@@ -3,6 +3,7 @@
using System.Threading;
using System.Threading.Tasks;
using System.Buffers;
+using System.Diagnostics;
using System.Linq;
using System.Threading.Channels;
using Grpc.Core;
@@ -19,51 +20,139 @@ public class StreamingHubClientOptions
public IMagicOnionSerializerProvider SerializerProvider { get; }
public IMagicOnionClientLogger Logger { get; }
- public TimeSpan? HeartbeatInterval { get; }
- public Action>? HeartbeatReceivedFromServer { get; }
+ public TimeSpan? ClientHeartbeatInterval { get; }
+ public TimeSpan? ClientHeartbeatTimeout { get; }
+ public Action>? OnServerHeartbeatReceived { get; }
+ public Action? OnClientHeartbeatResponseReceived { get; }
+#if NET8_0_OR_GREATER
+ public TimeProvider? TimeProvider { get; }
+#endif
public StreamingHubClientOptions(string? host, CallOptions callOptions, IMagicOnionSerializerProvider serializerProvider, IMagicOnionClientLogger logger)
- : this(host, callOptions, serializerProvider, logger, default, default)
+#if NET8_0_OR_GREATER
+ : this(host, callOptions, serializerProvider, logger, default, default, default, default, default)
+#else
+ : this(host, callOptions, serializerProvider, logger, default, default, default, default)
+#endif
{
}
- public StreamingHubClientOptions(string? host, CallOptions callOptions, IMagicOnionSerializerProvider serializerProvider, IMagicOnionClientLogger logger, TimeSpan? heartbeatInterval, Action>? heartbeatReceivedFromServer)
+#if NET8_0_OR_GREATER
+ public StreamingHubClientOptions(string? host, CallOptions callOptions, IMagicOnionSerializerProvider serializerProvider, IMagicOnionClientLogger logger, TimeSpan? clientHeartbeatInterval, TimeSpan? clientHeartbeatTimeout, Action>? onServerHeartbeatReceived, Action? onClientHeartbeatResponseReceived,TimeProvider? timeProvider)
+#else
+ public StreamingHubClientOptions(string? host, CallOptions callOptions, IMagicOnionSerializerProvider serializerProvider, IMagicOnionClientLogger logger, TimeSpan? clientHeartbeatInterval, TimeSpan? clientHeartbeatTimeout, Action>? onServerHeartbeatReceived, Action? onClientHeartbeatResponseReceived)
+#endif
{
Host = host;
CallOptions = callOptions;
SerializerProvider = serializerProvider ?? throw new ArgumentNullException(nameof(serializerProvider));
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
- HeartbeatInterval = heartbeatInterval;
- HeartbeatReceivedFromServer = heartbeatReceivedFromServer;
+ ClientHeartbeatInterval = clientHeartbeatInterval;
+ ClientHeartbeatTimeout = clientHeartbeatTimeout;
+ OnServerHeartbeatReceived = onServerHeartbeatReceived;
+ OnClientHeartbeatResponseReceived = onClientHeartbeatResponseReceived;
+#if NET8_0_OR_GREATER
+ TimeProvider = timeProvider;
+#endif
}
public static StreamingHubClientOptions CreateWithDefault(string? host = default, CallOptions callOptions = default, IMagicOnionSerializerProvider? serializerProvider = default, IMagicOnionClientLogger? logger = default)
=> new(host, callOptions, serializerProvider ?? MagicOnionSerializerProvider.Default, logger ?? NullMagicOnionClientLogger.Instance);
public StreamingHubClientOptions WithHost(string? host)
- => new(host, CallOptions, SerializerProvider, Logger, HeartbeatInterval, HeartbeatReceivedFromServer);
+ => new(host, CallOptions, SerializerProvider, Logger
+ , ClientHeartbeatInterval, ClientHeartbeatTimeout, OnServerHeartbeatReceived, OnClientHeartbeatResponseReceived
+#if NET8_0_OR_GREATER
+ , TimeProvider
+#endif
+ );
public StreamingHubClientOptions WithCallOptions(CallOptions callOptions)
- => new(Host, callOptions, SerializerProvider, Logger, HeartbeatInterval, HeartbeatReceivedFromServer);
+ => new(Host, callOptions, SerializerProvider, Logger
+ , ClientHeartbeatInterval, ClientHeartbeatTimeout, OnServerHeartbeatReceived, OnClientHeartbeatResponseReceived
+#if NET8_0_OR_GREATER
+ , TimeProvider
+#endif
+ );
public StreamingHubClientOptions WithSerializerProvider(IMagicOnionSerializerProvider serializerProvider)
- => new(Host, CallOptions, serializerProvider, Logger, HeartbeatInterval, HeartbeatReceivedFromServer);
+ => new(
+ Host, CallOptions, serializerProvider, Logger
+ , ClientHeartbeatInterval, ClientHeartbeatTimeout, OnServerHeartbeatReceived, OnClientHeartbeatResponseReceived
+#if NET8_0_OR_GREATER
+ , TimeProvider
+#endif
+ );
public StreamingHubClientOptions WithLogger(IMagicOnionClientLogger logger)
- => new(Host, CallOptions, SerializerProvider, logger, HeartbeatInterval, HeartbeatReceivedFromServer);
+ => new(Host, CallOptions, SerializerProvider, logger
+ , ClientHeartbeatInterval, ClientHeartbeatTimeout, OnServerHeartbeatReceived, OnClientHeartbeatResponseReceived
+#if NET8_0_OR_GREATER
+ , TimeProvider
+#endif
+ );
///
/// Sets a heartbeat interval. If a value is , the heartbeat from the client is disabled.
///
///
///
- public StreamingHubClientOptions WithHeartbeatInterval(TimeSpan? interval)
- => new(Host, CallOptions, SerializerProvider, Logger, interval, HeartbeatReceivedFromServer);
+ public StreamingHubClientOptions WithClientHeartbeatInterval(TimeSpan? interval)
+ => new(Host, CallOptions, SerializerProvider, Logger
+ , interval, ClientHeartbeatTimeout, OnServerHeartbeatReceived, OnClientHeartbeatResponseReceived
+#if NET8_0_OR_GREATER
+ , TimeProvider
+#endif
+ );
+
+ ///
+ /// Sets a heartbeat timeout period. If a value is , the client does not time out.
+ ///
+ ///
+ ///
+ public StreamingHubClientOptions WithClientHeartbeatTimeout(TimeSpan? timeout)
+ => new(Host, CallOptions, SerializerProvider, Logger
+ , ClientHeartbeatInterval, timeout, OnServerHeartbeatReceived, OnClientHeartbeatResponseReceived
+#if NET8_0_OR_GREATER
+ , TimeProvider
+#endif
+ );
///
/// Sets a heartbeat callback. If additional metadata is provided by the server in the heartbeat message, this metadata is provided as an argument.
///
- ///
+ ///
///
- public StreamingHubClientOptions WithHeartbeatReceived(Action>? onHeartbeatReceived)
- => new(Host, CallOptions, SerializerProvider, Logger, HeartbeatInterval, onHeartbeatReceived);
+ public StreamingHubClientOptions WithServerHeartbeatReceived(Action>? onServerHeartbeatReceived)
+ => new(Host, CallOptions, SerializerProvider, Logger
+ , ClientHeartbeatInterval, ClientHeartbeatTimeout, onServerHeartbeatReceived, OnClientHeartbeatResponseReceived
+#if NET8_0_OR_GREATER
+ , TimeProvider
+#endif
+ );
+
+ ///
+ /// Sets a client heartbeat response callback.
+ ///
+ ///
+ ///
+ public StreamingHubClientOptions WithClientHeartbeatResponseReceived(Action? onClientHeartbeatResponseReceived)
+ => new(Host, CallOptions, SerializerProvider, Logger
+ , ClientHeartbeatInterval, ClientHeartbeatTimeout, OnServerHeartbeatReceived, onClientHeartbeatResponseReceived
+#if NET8_0_OR_GREATER
+ , TimeProvider
+#endif
+ );
+
+#if NET8_0_OR_GREATER
+ ///
+ /// Sets a
+ ///
+ ///
+ ///
+ public StreamingHubClientOptions WithTimeProvider(TimeProvider timeProvider)
+ => new(Host, CallOptions, SerializerProvider, Logger
+ , ClientHeartbeatInterval, ClientHeartbeatTimeout, OnServerHeartbeatReceived, OnClientHeartbeatResponseReceived
+ , timeProvider
+ );
+#endif
}
public abstract class StreamingHubClientBase
@@ -83,21 +172,18 @@ public abstract class StreamingHubClientBase
readonly Dictionary responseFutures = new();
readonly TaskCompletionSource waitForDisconnect = new();
readonly CancellationTokenSource cancellationTokenSource = new();
-
readonly Dictionary postCallbackCache = new();
- SendOrPostCallback? heartbeatCallbackCache;
+
int messageIdSequence = 0;
bool disposed;
- Task? heartbeatTask;
- DateTimeOffset lastHeartbeatSentAt;
-
readonly Channel writerQueue = Channel.CreateUnbounded(new UnboundedChannelOptions() { SingleReader = true, SingleWriter = false, AllowSynchronousContinuations = false });
Task? writerTask;
IClientStreamWriter writer = default!;
IAsyncStreamReader reader = default!;
+ StreamingHubClientHeartbeatManager heartbeatManager = default!;
Task subscription = default!;
protected readonly TReceiver receiver;
@@ -183,12 +269,29 @@ static Method CreateConnectMethod(stri
async Task StartSubscribe(SynchronizationContext? syncContext, Task firstMoveNext)
{
- if (options.HeartbeatInterval is { } heartbeatInterval)
+ var cancellationToken = cancellationTokenSource.Token;
+
+ heartbeatManager = new StreamingHubClientHeartbeatManager(
+ writerQueue.Writer,
+ options.ClientHeartbeatInterval ?? TimeSpan.Zero /* Disable */,
+ options.ClientHeartbeatTimeout ?? Timeout.InfiniteTimeSpan,
+ options.OnServerHeartbeatReceived,
+ options.OnClientHeartbeatResponseReceived,
+ syncContext,
+ cancellationTokenSource.Token
+#if NET8_0_OR_GREATER
+ , options.TimeProvider ?? TimeProvider.System
+#endif
+ );
+
+ // Activate the Heartbeat Manager if enabled in the options.
+ if (options.ClientHeartbeatInterval is {} heartbeatInterval && heartbeatInterval > TimeSpan.Zero)
{
- heartbeatTask = RunHeartbeatLoopAsync(heartbeatInterval, cancellationTokenSource.Token);
+ heartbeatManager.StartClientHeartbeatLoop();
+ cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(heartbeatManager.TimeoutToken, cancellationTokenSource.Token).Token;
}
- writerTask = RunWriterLoopAsync(cancellationTokenSource.Token);
+ writerTask = RunWriterLoopAsync(cancellationToken);
var reader = this.reader;
try
@@ -214,7 +317,7 @@ async Task StartSubscribe(SynchronizationContext? syncContext, Task firstM
}
}
- moveNext = reader.MoveNext(cancellationTokenSource.Token);
+ moveNext = reader.MoveNext(cancellationToken);
}
}
catch (Exception ex)
@@ -236,6 +339,8 @@ async Task StartSubscribe(SynchronizationContext? syncContext, Task firstM
}
finally
{
+ heartbeatManager.Dispose();
+
try
{
#if !UNITY_WEBGL
@@ -255,10 +360,6 @@ async Task StartSubscribe(SynchronizationContext? syncContext, Task firstM
}
}
- // MessageFormat:
- // broadcast: [methodId, [argument]]
- // response: [messageId, methodId, response]
- // error-response: [messageId, statusCode, detail, StringMessage]
void ConsumeData(SynchronizationContext? syncContext, StreamingHubPayload payload)
{
var messageReader = new StreamingHubClientMessageReader(payload.Memory);
@@ -276,8 +377,11 @@ void ConsumeData(SynchronizationContext? syncContext, StreamingHubPayload payloa
case StreamingHubMessageType.ClientResultRequest:
ProcessClientResultRequest(syncContext, payload, ref messageReader);
break;
- case StreamingHubMessageType.Heartbeat:
- ProcessHeartbeat(syncContext, payload, ref messageReader);
+ case StreamingHubMessageType.ServerHeartbeat:
+ heartbeatManager.ProcessServerHeartbeat(payload);
+ break;
+ case StreamingHubMessageType.ClientHeartbeatResponse:
+ heartbeatManager.ProcessClientHeartbeatResponse(payload);
break;
}
}
@@ -386,46 +490,6 @@ void ProcessClientResultRequest(SynchronizationContext? syncContext, StreamingHu
}
}
- void ProcessHeartbeat(SynchronizationContext? syncContext, StreamingHubPayload payload, ref StreamingHubClientMessageReader messageReader)
- {
- var metadata = messageReader.ReadHeartbeat();
- if (this.options.HeartbeatReceivedFromServer is { } heartbeatReceived)
- {
- if (syncContext is null)
- {
- heartbeatReceived(metadata);
- StreamingHubPayloadPool.Shared.Return(payload);
- }
- else
- {
- heartbeatCallbackCache ??= CreateHeartbeatCallback(heartbeatReceived);
- syncContext.Post(heartbeatCallbackCache, payload);
- }
- }
- WriteHeartbeat();
- }
-
- SendOrPostCallback CreateHeartbeatCallback(Action> heartbeatReceivedAction) => (state) =>
- {
- var p = (StreamingHubPayload)state!;
- heartbeatReceivedAction(p.Memory.Slice(5));
- StreamingHubPayloadPool.Shared.Return(p);
- };
-
- async Task RunHeartbeatLoopAsync(TimeSpan heartbeatInterval, CancellationToken cancellationToken)
- {
- while (!cancellationToken.IsCancellationRequested)
- {
- await Task.Delay(heartbeatInterval, cancellationToken).ConfigureAwait(false);
- cancellationToken.ThrowIfCancellationRequested();
-
- if ((DateTimeOffset.UtcNow - lastHeartbeatSentAt) > heartbeatInterval)
- {
- WriteHeartbeat();
- }
- }
- }
-
async Task RunWriterLoopAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
@@ -440,15 +504,6 @@ async Task RunWriterLoopAsync(CancellationToken cancellationToken)
}
}
- void WriteHeartbeat()
- {
- if (disposed) return;
- var v = BuildHeartbeatMessage();
- _ = writerQueue.Writer.TryWrite(v);
-
- lastHeartbeatSentAt = DateTimeOffset.UtcNow;
- }
-
protected Task WriteMessageFireAndForgetTaskAsync(int methodId, TRequest message)
=> WriteMessageFireAndForgetValueTaskOfTAsync(methodId, message).AsTask();
@@ -674,12 +729,5 @@ StreamingHubPayload BuildClientResultResponseMessageForError(int methodId, Guid
StreamingHubMessageWriter.WriteClientResultResponseMessageForError(buffer, methodId, messageId, statusCode, detail, ex, messageSerializer);
return StreamingHubPayloadPool.Shared.RentOrCreate(buffer.WrittenSpan);
}
-
- StreamingHubPayload BuildHeartbeatMessage()
- {
- using var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter();
- StreamingHubMessageWriter.WriteHeartbeatMessageForClientToServer(buffer);
- return StreamingHubPayloadPool.Shared.RentOrCreate(buffer.WrittenSpan);
- }
}
}
diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientHeartbeatManager.cs b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientHeartbeatManager.cs
new file mode 100644
index 000000000..22506f613
--- /dev/null
+++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientHeartbeatManager.cs
@@ -0,0 +1,204 @@
+using System;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Channels;
+using System.Threading.Tasks;
+using MagicOnion.Internal;
+using MagicOnion.Internal.Buffers;
+using MessagePack;
+
+namespace MagicOnion.Client
+{
+ public readonly struct ClientHeartbeatEvent
+ {
+ ///
+ /// Gets the round trip time (RTT) between client and server.
+ ///
+ public TimeSpan RoundTripTime { get; }
+
+ public ClientHeartbeatEvent(long roundTripTimeMs)
+ {
+ RoundTripTime = TimeSpan.FromMilliseconds(roundTripTimeMs);
+ }
+ }
+
+ internal class StreamingHubClientHeartbeatManager : IDisposable
+ {
+ readonly CancellationTokenSource timeoutTokenSource;
+ readonly CancellationTokenSource shutdownTokenSource;
+ readonly TimeSpan heartbeatInterval;
+ readonly TimeSpan timeoutPeriod;
+ readonly Action>? onServerHeartbeatReceived;
+ readonly Action? onClientHeartbeatResponseReceived;
+ readonly SynchronizationContext? synchronizationContext;
+ readonly ChannelWriter writer;
+#if NET8_0_OR_GREATER
+ readonly TimeProvider timeProvider;
+#endif
+
+ SendOrPostCallback? serverHeartbeatCallbackCache;
+ SendOrPostCallback? clientHeartbeatResponseCallbackCache;
+ Task? heartbeatLoopTask;
+
+ public CancellationToken TimeoutToken => timeoutTokenSource.Token;
+
+ public StreamingHubClientHeartbeatManager(
+ ChannelWriter writer,
+ TimeSpan heartbeatInterval,
+ TimeSpan timeoutPeriod,
+ Action>? onServerHeartbeatReceived,
+ Action? onClientHeartbeatResponseReceived,
+ SynchronizationContext? synchronizationContext,
+ CancellationToken shutdownToken
+#if NET8_0_OR_GREATER
+ , TimeProvider timeProvider
+#endif
+ )
+ {
+ this.timeoutTokenSource = new(
+#if NET8_0_OR_GREATER
+ Timeout.InfiniteTimeSpan, timeProvider
+#endif
+ );
+ this.writer = writer;
+ this.heartbeatInterval = heartbeatInterval;
+ this.timeoutPeriod = timeoutPeriod;
+ this.onServerHeartbeatReceived = onServerHeartbeatReceived;
+ this.onClientHeartbeatResponseReceived = onClientHeartbeatResponseReceived;
+ this.synchronizationContext = synchronizationContext;
+ this.shutdownTokenSource = CancellationTokenSource.CreateLinkedTokenSource(shutdownToken, timeoutTokenSource.Token);
+#if NET8_0_OR_GREATER
+ this.timeProvider = timeProvider;
+#endif
+ }
+
+ public void StartClientHeartbeatLoop()
+ {
+ heartbeatLoopTask = RunClientHeartbeatLoopAsync();
+ }
+
+ async Task RunClientHeartbeatLoopAsync()
+ {
+ while (!shutdownTokenSource.IsCancellationRequested)
+ {
+ await Task.Delay(heartbeatInterval
+#if NET8_0_OR_GREATER
+ , timeProvider
+#endif
+ , shutdownTokenSource.Token).ConfigureAwait(false);
+
+ shutdownTokenSource.Token.ThrowIfCancellationRequested();
+
+ // Writes a ClientHeartbeat to the writer queue.
+ _ = writer.TryWrite(BuildClientHeartbeatMessage());
+
+ // Start/Restart the timeout cancellation timer.
+ timeoutTokenSource.CancelAfter(timeoutPeriod);
+ }
+ }
+
+ public void ProcessClientHeartbeatResponse(StreamingHubPayload payload)
+ {
+ if (shutdownTokenSource.IsCancellationRequested) return;
+
+ // Cancel the running timeout cancellation timer.
+ timeoutTokenSource.CancelAfter(Timeout.InfiniteTimeSpan);
+
+ if (onClientHeartbeatResponseReceived is { } heartbeatReceived)
+ {
+ clientHeartbeatResponseCallbackCache ??= CreateClientHeartbeatResponseCallback(heartbeatReceived);
+
+ if (synchronizationContext is null)
+ {
+ clientHeartbeatResponseCallbackCache(payload);
+ }
+ else
+ {
+ synchronizationContext.Post(clientHeartbeatResponseCallbackCache, payload);
+ }
+ }
+ }
+
+ public void ProcessServerHeartbeat(StreamingHubPayload payload)
+ {
+ if (shutdownTokenSource.IsCancellationRequested) return;
+
+ if (onServerHeartbeatReceived is { } heartbeatReceived)
+ {
+ serverHeartbeatCallbackCache ??= CreateServerHeartbeatCallback(heartbeatReceived);
+
+ if (synchronizationContext is null)
+ {
+ serverHeartbeatCallbackCache(payload);
+ }
+ else
+ {
+ synchronizationContext.Post(serverHeartbeatCallbackCache, payload);
+ }
+ }
+
+ // Writes a ServerHeartbeatResponse to the writer queue.
+ _ = writer.TryWrite(BuildServerHeartbeatMessage());
+ }
+
+ SendOrPostCallback CreateClientHeartbeatResponseCallback(Action heartbeatReceivedAction) => (state) =>
+ {
+ var p = (StreamingHubPayload)state!;
+
+ var reader = new StreamingHubClientMessageReader(p.Memory);
+ _ = reader.ReadMessageType();
+
+#if NET8_0_OR_GREATER
+ var now = timeProvider.GetUtcNow();
+#else
+ var now = DateTimeOffset.UtcNow;
+#endif
+ var sentAt = reader.ReadClientHeartbeatResponse();
+ var elapsed = now.ToUnixTimeMilliseconds() - sentAt;
+
+ heartbeatReceivedAction(new ClientHeartbeatEvent(elapsed));
+ StreamingHubPayloadPool.Shared.Return(p);
+ };
+
+ SendOrPostCallback CreateServerHeartbeatCallback(Action> heartbeatReceivedAction) => (state) =>
+ {
+ var p = (StreamingHubPayload)state!;
+ var remain = p.Memory.Slice(5); // header
+ heartbeatReceivedAction(remain);
+ StreamingHubPayloadPool.Shared.Return(p);
+ };
+
+ StreamingHubPayload BuildServerHeartbeatMessage()
+ {
+ using var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter();
+ StreamingHubMessageWriter.WriteServerHeartbeatMessageResponse(buffer);
+ return StreamingHubPayloadPool.Shared.RentOrCreate(buffer.WrittenSpan);
+ }
+
+ StreamingHubPayload BuildClientHeartbeatMessage()
+ {
+ using var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter();
+ StreamingHubMessageWriter.WriteClientHeartbeatMessageHeader(buffer);
+
+#if NET8_0_OR_GREATER
+ var now = timeProvider.GetUtcNow();
+#else
+ var now = DateTimeOffset.UtcNow;
+#endif
+
+ // Extra: [SentAt(long)]
+ var writer = new MessagePackWriter(buffer);
+ writer.WriteArrayHeader(1);
+ writer.Write(now.ToUnixTimeMilliseconds());
+ writer.Flush();
+ return StreamingHubPayloadPool.Shared.RentOrCreate(buffer.WrittenSpan);
+ }
+
+ public void Dispose()
+ {
+ shutdownTokenSource.Cancel();
+ shutdownTokenSource.Dispose();
+ timeoutTokenSource.Dispose();
+ }
+ }
+}
diff --git a/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientHeartbeatManager.cs.meta b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientHeartbeatManager.cs.meta
new file mode 100644
index 000000000..e26f1a2aa
--- /dev/null
+++ b/src/MagicOnion.Client.Unity/Assets/Scripts/MagicOnion/MagicOnion.Client/StreamingHubClientHeartbeatManager.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f997866831803b34aa603952177c34e2
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/src/MagicOnion.Client/StreamingHubClientBase.cs b/src/MagicOnion.Client/StreamingHubClientBase.cs
index 51905894f..613909c7a 100644
--- a/src/MagicOnion.Client/StreamingHubClientBase.cs
+++ b/src/MagicOnion.Client/StreamingHubClientBase.cs
@@ -3,6 +3,7 @@
using System.Threading;
using System.Threading.Tasks;
using System.Buffers;
+using System.Diagnostics;
using System.Linq;
using System.Threading.Channels;
using Grpc.Core;
@@ -19,51 +20,139 @@ public class StreamingHubClientOptions
public IMagicOnionSerializerProvider SerializerProvider { get; }
public IMagicOnionClientLogger Logger { get; }
- public TimeSpan? HeartbeatInterval { get; }
- public Action>? HeartbeatReceivedFromServer { get; }
+ public TimeSpan? ClientHeartbeatInterval { get; }
+ public TimeSpan? ClientHeartbeatTimeout { get; }
+ public Action>? OnServerHeartbeatReceived { get; }
+ public Action? OnClientHeartbeatResponseReceived { get; }
+#if NET8_0_OR_GREATER
+ public TimeProvider? TimeProvider { get; }
+#endif
public StreamingHubClientOptions(string? host, CallOptions callOptions, IMagicOnionSerializerProvider serializerProvider, IMagicOnionClientLogger logger)
- : this(host, callOptions, serializerProvider, logger, default, default)
+#if NET8_0_OR_GREATER
+ : this(host, callOptions, serializerProvider, logger, default, default, default, default, default)
+#else
+ : this(host, callOptions, serializerProvider, logger, default, default, default, default)
+#endif
{
}
- public StreamingHubClientOptions(string? host, CallOptions callOptions, IMagicOnionSerializerProvider serializerProvider, IMagicOnionClientLogger logger, TimeSpan? heartbeatInterval, Action>? heartbeatReceivedFromServer)
+#if NET8_0_OR_GREATER
+ public StreamingHubClientOptions(string? host, CallOptions callOptions, IMagicOnionSerializerProvider serializerProvider, IMagicOnionClientLogger logger, TimeSpan? clientHeartbeatInterval, TimeSpan? clientHeartbeatTimeout, Action>? onServerHeartbeatReceived, Action? onClientHeartbeatResponseReceived,TimeProvider? timeProvider)
+#else
+ public StreamingHubClientOptions(string? host, CallOptions callOptions, IMagicOnionSerializerProvider serializerProvider, IMagicOnionClientLogger logger, TimeSpan? clientHeartbeatInterval, TimeSpan? clientHeartbeatTimeout, Action>? onServerHeartbeatReceived, Action? onClientHeartbeatResponseReceived)
+#endif
{
Host = host;
CallOptions = callOptions;
SerializerProvider = serializerProvider ?? throw new ArgumentNullException(nameof(serializerProvider));
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
- HeartbeatInterval = heartbeatInterval;
- HeartbeatReceivedFromServer = heartbeatReceivedFromServer;
+ ClientHeartbeatInterval = clientHeartbeatInterval;
+ ClientHeartbeatTimeout = clientHeartbeatTimeout;
+ OnServerHeartbeatReceived = onServerHeartbeatReceived;
+ OnClientHeartbeatResponseReceived = onClientHeartbeatResponseReceived;
+#if NET8_0_OR_GREATER
+ TimeProvider = timeProvider;
+#endif
}
public static StreamingHubClientOptions CreateWithDefault(string? host = default, CallOptions callOptions = default, IMagicOnionSerializerProvider? serializerProvider = default, IMagicOnionClientLogger? logger = default)
=> new(host, callOptions, serializerProvider ?? MagicOnionSerializerProvider.Default, logger ?? NullMagicOnionClientLogger.Instance);
public StreamingHubClientOptions WithHost(string? host)
- => new(host, CallOptions, SerializerProvider, Logger, HeartbeatInterval, HeartbeatReceivedFromServer);
+ => new(host, CallOptions, SerializerProvider, Logger
+ , ClientHeartbeatInterval, ClientHeartbeatTimeout, OnServerHeartbeatReceived, OnClientHeartbeatResponseReceived
+#if NET8_0_OR_GREATER
+ , TimeProvider
+#endif
+ );
public StreamingHubClientOptions WithCallOptions(CallOptions callOptions)
- => new(Host, callOptions, SerializerProvider, Logger, HeartbeatInterval, HeartbeatReceivedFromServer);
+ => new(Host, callOptions, SerializerProvider, Logger
+ , ClientHeartbeatInterval, ClientHeartbeatTimeout, OnServerHeartbeatReceived, OnClientHeartbeatResponseReceived
+#if NET8_0_OR_GREATER
+ , TimeProvider
+#endif
+ );
public StreamingHubClientOptions WithSerializerProvider(IMagicOnionSerializerProvider serializerProvider)
- => new(Host, CallOptions, serializerProvider, Logger, HeartbeatInterval, HeartbeatReceivedFromServer);
+ => new(
+ Host, CallOptions, serializerProvider, Logger
+ , ClientHeartbeatInterval, ClientHeartbeatTimeout, OnServerHeartbeatReceived, OnClientHeartbeatResponseReceived
+#if NET8_0_OR_GREATER
+ , TimeProvider
+#endif
+ );
public StreamingHubClientOptions WithLogger(IMagicOnionClientLogger logger)
- => new(Host, CallOptions, SerializerProvider, logger, HeartbeatInterval, HeartbeatReceivedFromServer);
+ => new(Host, CallOptions, SerializerProvider, logger
+ , ClientHeartbeatInterval, ClientHeartbeatTimeout, OnServerHeartbeatReceived, OnClientHeartbeatResponseReceived
+#if NET8_0_OR_GREATER
+ , TimeProvider
+#endif
+ );
///
/// Sets a heartbeat interval. If a value is , the heartbeat from the client is disabled.
///
///
///
- public StreamingHubClientOptions WithHeartbeatInterval(TimeSpan? interval)
- => new(Host, CallOptions, SerializerProvider, Logger, interval, HeartbeatReceivedFromServer);
+ public StreamingHubClientOptions WithClientHeartbeatInterval(TimeSpan? interval)
+ => new(Host, CallOptions, SerializerProvider, Logger
+ , interval, ClientHeartbeatTimeout, OnServerHeartbeatReceived, OnClientHeartbeatResponseReceived
+#if NET8_0_OR_GREATER
+ , TimeProvider
+#endif
+ );
+
+ ///
+ /// Sets a heartbeat timeout period. If a value is , the client does not time out.
+ ///
+ ///
+ ///
+ public StreamingHubClientOptions WithClientHeartbeatTimeout(TimeSpan? timeout)
+ => new(Host, CallOptions, SerializerProvider, Logger
+ , ClientHeartbeatInterval, timeout, OnServerHeartbeatReceived, OnClientHeartbeatResponseReceived
+#if NET8_0_OR_GREATER
+ , TimeProvider
+#endif
+ );
///
/// Sets a heartbeat callback. If additional metadata is provided by the server in the heartbeat message, this metadata is provided as an argument.
///
- ///
+ ///
///
- public StreamingHubClientOptions WithHeartbeatReceived(Action>? onHeartbeatReceived)
- => new(Host, CallOptions, SerializerProvider, Logger, HeartbeatInterval, onHeartbeatReceived);
+ public StreamingHubClientOptions WithServerHeartbeatReceived(Action>? onServerHeartbeatReceived)
+ => new(Host, CallOptions, SerializerProvider, Logger
+ , ClientHeartbeatInterval, ClientHeartbeatTimeout, onServerHeartbeatReceived, OnClientHeartbeatResponseReceived
+#if NET8_0_OR_GREATER
+ , TimeProvider
+#endif
+ );
+
+ ///
+ /// Sets a client heartbeat response callback.
+ ///
+ ///
+ ///
+ public StreamingHubClientOptions WithClientHeartbeatResponseReceived(Action? onClientHeartbeatResponseReceived)
+ => new(Host, CallOptions, SerializerProvider, Logger
+ , ClientHeartbeatInterval, ClientHeartbeatTimeout, OnServerHeartbeatReceived, onClientHeartbeatResponseReceived
+#if NET8_0_OR_GREATER
+ , TimeProvider
+#endif
+ );
+
+#if NET8_0_OR_GREATER
+ ///
+ /// Sets a
+ ///
+ ///
+ ///
+ public StreamingHubClientOptions WithTimeProvider(TimeProvider timeProvider)
+ => new(Host, CallOptions, SerializerProvider, Logger
+ , ClientHeartbeatInterval, ClientHeartbeatTimeout, OnServerHeartbeatReceived, OnClientHeartbeatResponseReceived
+ , timeProvider
+ );
+#endif
}
public abstract class StreamingHubClientBase
@@ -83,21 +172,18 @@ public abstract class StreamingHubClientBase
readonly Dictionary responseFutures = new();
readonly TaskCompletionSource waitForDisconnect = new();
readonly CancellationTokenSource cancellationTokenSource = new();
-
readonly Dictionary postCallbackCache = new();
- SendOrPostCallback? heartbeatCallbackCache;
+
int messageIdSequence = 0;
bool disposed;
- Task? heartbeatTask;
- DateTimeOffset lastHeartbeatSentAt;
-
readonly Channel writerQueue = Channel.CreateUnbounded(new UnboundedChannelOptions() { SingleReader = true, SingleWriter = false, AllowSynchronousContinuations = false });
Task? writerTask;
IClientStreamWriter writer = default!;
IAsyncStreamReader reader = default!;
+ StreamingHubClientHeartbeatManager heartbeatManager = default!;
Task subscription = default!;
protected readonly TReceiver receiver;
@@ -183,12 +269,29 @@ static Method CreateConnectMethod(stri
async Task StartSubscribe(SynchronizationContext? syncContext, Task firstMoveNext)
{
- if (options.HeartbeatInterval is { } heartbeatInterval)
+ var cancellationToken = cancellationTokenSource.Token;
+
+ heartbeatManager = new StreamingHubClientHeartbeatManager(
+ writerQueue.Writer,
+ options.ClientHeartbeatInterval ?? TimeSpan.Zero /* Disable */,
+ options.ClientHeartbeatTimeout ?? Timeout.InfiniteTimeSpan,
+ options.OnServerHeartbeatReceived,
+ options.OnClientHeartbeatResponseReceived,
+ syncContext,
+ cancellationTokenSource.Token
+#if NET8_0_OR_GREATER
+ , options.TimeProvider ?? TimeProvider.System
+#endif
+ );
+
+ // Activate the Heartbeat Manager if enabled in the options.
+ if (options.ClientHeartbeatInterval is {} heartbeatInterval && heartbeatInterval > TimeSpan.Zero)
{
- heartbeatTask = RunHeartbeatLoopAsync(heartbeatInterval, cancellationTokenSource.Token);
+ heartbeatManager.StartClientHeartbeatLoop();
+ cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(heartbeatManager.TimeoutToken, cancellationTokenSource.Token).Token;
}
- writerTask = RunWriterLoopAsync(cancellationTokenSource.Token);
+ writerTask = RunWriterLoopAsync(cancellationToken);
var reader = this.reader;
try
@@ -214,7 +317,7 @@ async Task StartSubscribe(SynchronizationContext? syncContext, Task firstM
}
}
- moveNext = reader.MoveNext(cancellationTokenSource.Token);
+ moveNext = reader.MoveNext(cancellationToken);
}
}
catch (Exception ex)
@@ -236,6 +339,8 @@ async Task StartSubscribe(SynchronizationContext? syncContext, Task firstM
}
finally
{
+ heartbeatManager.Dispose();
+
try
{
#if !UNITY_WEBGL
@@ -255,10 +360,6 @@ async Task StartSubscribe(SynchronizationContext? syncContext, Task firstM
}
}
- // MessageFormat:
- // broadcast: [methodId, [argument]]
- // response: [messageId, methodId, response]
- // error-response: [messageId, statusCode, detail, StringMessage]
void ConsumeData(SynchronizationContext? syncContext, StreamingHubPayload payload)
{
var messageReader = new StreamingHubClientMessageReader(payload.Memory);
@@ -276,8 +377,11 @@ void ConsumeData(SynchronizationContext? syncContext, StreamingHubPayload payloa
case StreamingHubMessageType.ClientResultRequest:
ProcessClientResultRequest(syncContext, payload, ref messageReader);
break;
- case StreamingHubMessageType.Heartbeat:
- ProcessHeartbeat(syncContext, payload, ref messageReader);
+ case StreamingHubMessageType.ServerHeartbeat:
+ heartbeatManager.ProcessServerHeartbeat(payload);
+ break;
+ case StreamingHubMessageType.ClientHeartbeatResponse:
+ heartbeatManager.ProcessClientHeartbeatResponse(payload);
break;
}
}
@@ -386,46 +490,6 @@ void ProcessClientResultRequest(SynchronizationContext? syncContext, StreamingHu
}
}
- void ProcessHeartbeat(SynchronizationContext? syncContext, StreamingHubPayload payload, ref StreamingHubClientMessageReader messageReader)
- {
- var metadata = messageReader.ReadHeartbeat();
- if (this.options.HeartbeatReceivedFromServer is { } heartbeatReceived)
- {
- if (syncContext is null)
- {
- heartbeatReceived(metadata);
- StreamingHubPayloadPool.Shared.Return(payload);
- }
- else
- {
- heartbeatCallbackCache ??= CreateHeartbeatCallback(heartbeatReceived);
- syncContext.Post(heartbeatCallbackCache, payload);
- }
- }
- WriteHeartbeat();
- }
-
- SendOrPostCallback CreateHeartbeatCallback(Action> heartbeatReceivedAction) => (state) =>
- {
- var p = (StreamingHubPayload)state!;
- heartbeatReceivedAction(p.Memory.Slice(5));
- StreamingHubPayloadPool.Shared.Return(p);
- };
-
- async Task RunHeartbeatLoopAsync(TimeSpan heartbeatInterval, CancellationToken cancellationToken)
- {
- while (!cancellationToken.IsCancellationRequested)
- {
- await Task.Delay(heartbeatInterval, cancellationToken).ConfigureAwait(false);
- cancellationToken.ThrowIfCancellationRequested();
-
- if ((DateTimeOffset.UtcNow - lastHeartbeatSentAt) > heartbeatInterval)
- {
- WriteHeartbeat();
- }
- }
- }
-
async Task RunWriterLoopAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
@@ -440,15 +504,6 @@ async Task RunWriterLoopAsync(CancellationToken cancellationToken)
}
}
- void WriteHeartbeat()
- {
- if (disposed) return;
- var v = BuildHeartbeatMessage();
- _ = writerQueue.Writer.TryWrite(v);
-
- lastHeartbeatSentAt = DateTimeOffset.UtcNow;
- }
-
protected Task WriteMessageFireAndForgetTaskAsync(int methodId, TRequest message)
=> WriteMessageFireAndForgetValueTaskOfTAsync(methodId, message).AsTask();
@@ -674,12 +729,5 @@ StreamingHubPayload BuildClientResultResponseMessageForError(int methodId, Guid
StreamingHubMessageWriter.WriteClientResultResponseMessageForError(buffer, methodId, messageId, statusCode, detail, ex, messageSerializer);
return StreamingHubPayloadPool.Shared.RentOrCreate(buffer.WrittenSpan);
}
-
- StreamingHubPayload BuildHeartbeatMessage()
- {
- using var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter();
- StreamingHubMessageWriter.WriteHeartbeatMessageForClientToServer(buffer);
- return StreamingHubPayloadPool.Shared.RentOrCreate(buffer.WrittenSpan);
- }
}
}
diff --git a/src/MagicOnion.Client/StreamingHubClientHeartbeatManager.cs b/src/MagicOnion.Client/StreamingHubClientHeartbeatManager.cs
new file mode 100644
index 000000000..22506f613
--- /dev/null
+++ b/src/MagicOnion.Client/StreamingHubClientHeartbeatManager.cs
@@ -0,0 +1,204 @@
+using System;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Channels;
+using System.Threading.Tasks;
+using MagicOnion.Internal;
+using MagicOnion.Internal.Buffers;
+using MessagePack;
+
+namespace MagicOnion.Client
+{
+ public readonly struct ClientHeartbeatEvent
+ {
+ ///
+ /// Gets the round trip time (RTT) between client and server.
+ ///
+ public TimeSpan RoundTripTime { get; }
+
+ public ClientHeartbeatEvent(long roundTripTimeMs)
+ {
+ RoundTripTime = TimeSpan.FromMilliseconds(roundTripTimeMs);
+ }
+ }
+
+ internal class StreamingHubClientHeartbeatManager : IDisposable
+ {
+ readonly CancellationTokenSource timeoutTokenSource;
+ readonly CancellationTokenSource shutdownTokenSource;
+ readonly TimeSpan heartbeatInterval;
+ readonly TimeSpan timeoutPeriod;
+ readonly Action>? onServerHeartbeatReceived;
+ readonly Action? onClientHeartbeatResponseReceived;
+ readonly SynchronizationContext? synchronizationContext;
+ readonly ChannelWriter writer;
+#if NET8_0_OR_GREATER
+ readonly TimeProvider timeProvider;
+#endif
+
+ SendOrPostCallback? serverHeartbeatCallbackCache;
+ SendOrPostCallback? clientHeartbeatResponseCallbackCache;
+ Task? heartbeatLoopTask;
+
+ public CancellationToken TimeoutToken => timeoutTokenSource.Token;
+
+ public StreamingHubClientHeartbeatManager(
+ ChannelWriter writer,
+ TimeSpan heartbeatInterval,
+ TimeSpan timeoutPeriod,
+ Action>? onServerHeartbeatReceived,
+ Action? onClientHeartbeatResponseReceived,
+ SynchronizationContext? synchronizationContext,
+ CancellationToken shutdownToken
+#if NET8_0_OR_GREATER
+ , TimeProvider timeProvider
+#endif
+ )
+ {
+ this.timeoutTokenSource = new(
+#if NET8_0_OR_GREATER
+ Timeout.InfiniteTimeSpan, timeProvider
+#endif
+ );
+ this.writer = writer;
+ this.heartbeatInterval = heartbeatInterval;
+ this.timeoutPeriod = timeoutPeriod;
+ this.onServerHeartbeatReceived = onServerHeartbeatReceived;
+ this.onClientHeartbeatResponseReceived = onClientHeartbeatResponseReceived;
+ this.synchronizationContext = synchronizationContext;
+ this.shutdownTokenSource = CancellationTokenSource.CreateLinkedTokenSource(shutdownToken, timeoutTokenSource.Token);
+#if NET8_0_OR_GREATER
+ this.timeProvider = timeProvider;
+#endif
+ }
+
+ public void StartClientHeartbeatLoop()
+ {
+ heartbeatLoopTask = RunClientHeartbeatLoopAsync();
+ }
+
+ async Task RunClientHeartbeatLoopAsync()
+ {
+ while (!shutdownTokenSource.IsCancellationRequested)
+ {
+ await Task.Delay(heartbeatInterval
+#if NET8_0_OR_GREATER
+ , timeProvider
+#endif
+ , shutdownTokenSource.Token).ConfigureAwait(false);
+
+ shutdownTokenSource.Token.ThrowIfCancellationRequested();
+
+ // Writes a ClientHeartbeat to the writer queue.
+ _ = writer.TryWrite(BuildClientHeartbeatMessage());
+
+ // Start/Restart the timeout cancellation timer.
+ timeoutTokenSource.CancelAfter(timeoutPeriod);
+ }
+ }
+
+ public void ProcessClientHeartbeatResponse(StreamingHubPayload payload)
+ {
+ if (shutdownTokenSource.IsCancellationRequested) return;
+
+ // Cancel the running timeout cancellation timer.
+ timeoutTokenSource.CancelAfter(Timeout.InfiniteTimeSpan);
+
+ if (onClientHeartbeatResponseReceived is { } heartbeatReceived)
+ {
+ clientHeartbeatResponseCallbackCache ??= CreateClientHeartbeatResponseCallback(heartbeatReceived);
+
+ if (synchronizationContext is null)
+ {
+ clientHeartbeatResponseCallbackCache(payload);
+ }
+ else
+ {
+ synchronizationContext.Post(clientHeartbeatResponseCallbackCache, payload);
+ }
+ }
+ }
+
+ public void ProcessServerHeartbeat(StreamingHubPayload payload)
+ {
+ if (shutdownTokenSource.IsCancellationRequested) return;
+
+ if (onServerHeartbeatReceived is { } heartbeatReceived)
+ {
+ serverHeartbeatCallbackCache ??= CreateServerHeartbeatCallback(heartbeatReceived);
+
+ if (synchronizationContext is null)
+ {
+ serverHeartbeatCallbackCache(payload);
+ }
+ else
+ {
+ synchronizationContext.Post(serverHeartbeatCallbackCache, payload);
+ }
+ }
+
+ // Writes a ServerHeartbeatResponse to the writer queue.
+ _ = writer.TryWrite(BuildServerHeartbeatMessage());
+ }
+
+ SendOrPostCallback CreateClientHeartbeatResponseCallback(Action heartbeatReceivedAction) => (state) =>
+ {
+ var p = (StreamingHubPayload)state!;
+
+ var reader = new StreamingHubClientMessageReader(p.Memory);
+ _ = reader.ReadMessageType();
+
+#if NET8_0_OR_GREATER
+ var now = timeProvider.GetUtcNow();
+#else
+ var now = DateTimeOffset.UtcNow;
+#endif
+ var sentAt = reader.ReadClientHeartbeatResponse();
+ var elapsed = now.ToUnixTimeMilliseconds() - sentAt;
+
+ heartbeatReceivedAction(new ClientHeartbeatEvent(elapsed));
+ StreamingHubPayloadPool.Shared.Return(p);
+ };
+
+ SendOrPostCallback CreateServerHeartbeatCallback(Action> heartbeatReceivedAction) => (state) =>
+ {
+ var p = (StreamingHubPayload)state!;
+ var remain = p.Memory.Slice(5); // header
+ heartbeatReceivedAction(remain);
+ StreamingHubPayloadPool.Shared.Return(p);
+ };
+
+ StreamingHubPayload BuildServerHeartbeatMessage()
+ {
+ using var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter();
+ StreamingHubMessageWriter.WriteServerHeartbeatMessageResponse(buffer);
+ return StreamingHubPayloadPool.Shared.RentOrCreate(buffer.WrittenSpan);
+ }
+
+ StreamingHubPayload BuildClientHeartbeatMessage()
+ {
+ using var buffer = ArrayPoolBufferWriter.RentThreadStaticWriter();
+ StreamingHubMessageWriter.WriteClientHeartbeatMessageHeader(buffer);
+
+#if NET8_0_OR_GREATER
+ var now = timeProvider.GetUtcNow();
+#else
+ var now = DateTimeOffset.UtcNow;
+#endif
+
+ // Extra: [SentAt(long)]
+ var writer = new MessagePackWriter(buffer);
+ writer.WriteArrayHeader(1);
+ writer.Write(now.ToUnixTimeMilliseconds());
+ writer.Flush();
+ return StreamingHubPayloadPool.Shared.RentOrCreate(buffer.WrittenSpan);
+ }
+
+ public void Dispose()
+ {
+ shutdownTokenSource.Cancel();
+ shutdownTokenSource.Dispose();
+ timeoutTokenSource.Dispose();
+ }
+ }
+}
diff --git a/src/MagicOnion.Internal/StreamingHubClientMessageReader.cs b/src/MagicOnion.Internal/StreamingHubClientMessageReader.cs
index 247caec1f..5d4b59474 100644
--- a/src/MagicOnion.Internal/StreamingHubClientMessageReader.cs
+++ b/src/MagicOnion.Internal/StreamingHubClientMessageReader.cs
@@ -24,8 +24,9 @@ public StreamingHubMessageType ReadMessageType()
4 => StreamingHubMessageType.ResponseWithError,
5 => reader.ReadByte() switch
{
- 0x00 /* 0:ClientResultRequest */ => StreamingHubMessageType.ClientResultRequest,
- 0x7f /* 127:Heartbeat */ => StreamingHubMessageType.Heartbeat,
+ 0x00 /* 0:ClientResultRequest */ => StreamingHubMessageType.ClientResultRequest,
+ 0x7e /* 126:ClientHeartbeatResponse */ => StreamingHubMessageType.ClientHeartbeatResponse,
+ 0x7f /* 127:ServerHeartbeat */ => StreamingHubMessageType.ServerHeartbeat,
var x => throw new InvalidOperationException($"Unknown Type: {x}"),
},
_ => throw new InvalidOperationException($"Unknown message format: ArrayLength = {arrayLength}"),
@@ -73,7 +74,7 @@ public StreamingHubMessageType ReadMessageType()
return (clientRequestMessageId, methodId, data.Slice(offset));
}
- public ReadOnlyMemory ReadHeartbeat()
+ public ReadOnlyMemory ReadServerHeartbeat()
{
//var type = reader.ReadByte(); // Type is already read by ReadMessageType
reader.Skip(); // Dummy (1)
@@ -82,5 +83,20 @@ public ReadOnlyMemory ReadHeartbeat()
return data.Slice((int)reader.Consumed);
}
+
+ public long ReadClientHeartbeatResponse()
+ {
+ //var type = reader.ReadByte(); // Type is already read by ReadMessageType
+ reader.Skip(); // Dummy (1)
+ reader.Skip(); // Dummy (2)
+ reader.Skip(); // Dummy (3)
+
+ // Extra: [SentAt(long)]
+ var arrayLen = reader.ReadArrayHeader();
+ if (arrayLen == 0) throw new InvalidOperationException("Invalid client heartbeat response. An extra data is empty.");
+ var sentAt = reader.ReadInt64();
+
+ return sentAt;
+ }
}
}
diff --git a/src/MagicOnion.Internal/StreamingHubMessageWriter.cs b/src/MagicOnion.Internal/StreamingHubMessageWriter.cs
index 93832281e..9be226ff4 100644
--- a/src/MagicOnion.Internal/StreamingHubMessageWriter.cs
+++ b/src/MagicOnion.Internal/StreamingHubMessageWriter.cs
@@ -26,9 +26,13 @@ namespace MagicOnion.Internal
/// Array(5): [Type=0x00, Nil, ClientResultMessageId(Guid), MethodId(int), SerializedArguments]
///
/// -
- /// Heartbeat:
+ /// ServerHeartbeat/Request:
/// Array(5): [Type=0x7f, Nil, Nil, Nil, Extras]
///
+ /// -
+ /// ClientHeartbeat/Response:
+ /// Array(5): [Type=0x7e, Nil, Nil, Nil, [ClientTime(long)]]
+ ///
///
/// StreamingHub message formats (from Client to Server):
///
@@ -49,9 +53,13 @@ namespace MagicOnion.Internal
/// Array(4): [Type=0x01, ClientResultMessageId(Guid), MethodId(int), [StatusCode(int), Detail(string), Message(string)]]
///
/// -
- /// Heartbeat/Response:
+ /// ServerHeartbeat/Response:
/// Array(4): [Type=0x7f, Nil, Nil, Nil]
///
+ /// -
+ /// ClientHeartbeat/Request:
+ /// Array(4): [Type=0x7e, Nil, Nil, [ClientTime(long)]]
+ ///
///
///
internal static class StreamingHubMessageWriter
@@ -193,58 +201,138 @@ public static void WriteClientResultResponseMessageForError(IBufferWriter
// Array(5)[127, Nil, Nil, Nil, ]
- static ReadOnlySpan HeartbeatMessageForServerToClientHeader => new byte[] { 0x95, 0x7f, 0xc0, 0xc0, 0xc0 };
+ static ReadOnlySpan ServerHeartbeatMessageForServerToClientHeader => new byte[] { 0x95, 0x7f, 0xc0, 0xc0, 0xc0 };
///
- /// Writes a heartbeat message for sending from the server.
+ /// Writes a server heartbeat message for sending from the server.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void WriteHeartbeatMessageForServerToClientHeader(IBufferWriter bufferWriter)
+ public static void WriteServerHeartbeatMessageHeader(IBufferWriter bufferWriter)
{
- bufferWriter.Write(HeartbeatMessageForServerToClientHeader);
+ bufferWriter.Write(ServerHeartbeatMessageForServerToClientHeader);
//var writer = new MessagePackWriter(bufferWriter);
//writer.WriteArrayHeader(5);
//writer.Write(0x7f); // Type = 0x7f / 127 (Heartbeat)
+ //writer.WriteNil(); // Dummy
+ //writer.WriteNil(); // Dummy
+ //writer.WriteNil(); // Dummy
+ //writer.Flush();
+ // //
+ }
+
+ // Array(4)[127, Nil, Nil, Nil]
+ static ReadOnlySpan ServerHeartbeatMessageForClientToServer => new byte[] { 0x94, 0x7f, 0xc0, 0xc0, 0xc0 };
+
+ ///
+ /// Writes a server heartbeat message for sending response from the client.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void WriteServerHeartbeatMessageResponse(IBufferWriter bufferWriter)
+ {
+ bufferWriter.Write(ServerHeartbeatMessageForClientToServer);
+ //var writer = new MessagePackWriter(bufferWriter);
+ //writer.WriteArrayHeader(4);
+ //writer.Write(0x7f); // Type = 0x7f / 127 (Heartbeat)
//writer.WriteNil(); // Dummy
//writer.WriteNil(); // Dummy
//writer.WriteNil(); // Dummy
//writer.Flush();
}
- // Array(4)[127, Nil, Nil, Nil]
- static ReadOnlySpan HeartbeatMessageForClientToServer => new byte[] { 0x94, 0x7f, 0xc0, 0xc0, 0xc0 };
+ // Array(4)[0x7e(126), Nil, Nil, ]
+ static ReadOnlySpan ClientHeartbeatMessageHeader => new byte[] { 0x94, 0x7e, 0xc0, 0xc0 };
///
- /// Writes a heartbeat message for sending from the client.
+ /// Writes a client heartbeat message for sending from the client.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void WriteHeartbeatMessageForClientToServer(IBufferWriter bufferWriter)
+ public static void WriteClientHeartbeatMessageHeader(IBufferWriter bufferWriter)
{
- bufferWriter.Write(HeartbeatMessageForClientToServer);
+ bufferWriter.Write(ClientHeartbeatMessageHeader);
//var writer = new MessagePackWriter(bufferWriter);
//writer.WriteArrayHeader(4);
- //writer.Write(0x7f); // Type = 0x7f / 127 (Heartbeat)
+ //writer.Write(0x7f); // Type = 0x7e / 126 (ClientHeartbeat)
+ //writer.WriteNil(); // Dummy
+ //writer.WriteNil(); // Dummy
+ //writer.Flush();
+ // //
+ }
+
+ // Array(5)[0x7e(126), Nil, Nil, Nil, ]
+ static ReadOnlySpan ClientHeartbeatMessageResponseHeader => new byte[] { 0x95, 0x7e, 0xc0, 0xc0, 0xc0 };
+
+ ///
+ /// Writes a client heartbeat message for sending response from the server.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void WriteClientHeartbeatMessageResponseHeader(IBufferWriter bufferWriter)
+ {
+ bufferWriter.Write(ClientHeartbeatMessageResponseHeader);
+ //var writer = new MessagePackWriter(bufferWriter);
+ //writer.WriteArrayHeader(5);
+ //writer.Write(0x7f); // Type = 0x7e / 126 (Heartbeat)
//writer.WriteNil(); // Dummy
//writer.WriteNil(); // Dummy
//writer.WriteNil(); // Dummy
//writer.Flush();
+ // //
}
}
internal enum StreamingHubMessageType
{
- // Client to Server
+ ///
+ /// Request: Client -> Server
+ ///
Request,
+ ///
+ /// Request: Client -> Server / Fire-and-Forget
+ ///
RequestFireAndForget,
+ ///
+ /// Request: Client -> Server -> Client
+ ///
Response,
+ ///
+ /// Request: Client -> Server -(Error)-> Client
+ ///
ResponseWithError,
- HeartbeatResponse,
- // Server to Client
+ ///
+ /// Broadcast: Server -> Client
+ ///
Broadcast,
+
+ ///
+ /// ClientResult: Server -> Client
+ ///
ClientResultRequest,
+ ///
+ /// ClientResult: Server -> Client -> Server
+ ///
ClientResultResponse,
+ ///
+ /// ClientResult: Server -> Client -(Error)-> Server
+ ///
ClientResultResponseWithError,
- Heartbeat,
+
+
+ ///
+ /// Heartbeat: Server -> Client -> Server
+ ///
+ ServerHeartbeatResponse,
+ ///
+ /// Heartbeat: Server -> Client
+ ///
+ ServerHeartbeat,
+
+ ///
+ /// Heartbeat: Client -> Server
+ ///
+ ClientHeartbeat,
+ ///
+ /// Heartbeat: Client -> Server -> Client
+ ///
+ ClientHeartbeatResponse,
}
}
diff --git a/src/MagicOnion.Internal/StreamingHubPayload.cs b/src/MagicOnion.Internal/StreamingHubPayload.cs
index d3387100f..d4c5b1ee0 100644
--- a/src/MagicOnion.Internal/StreamingHubPayload.cs
+++ b/src/MagicOnion.Internal/StreamingHubPayload.cs
@@ -104,12 +104,21 @@ public void Initialize(ReadOnlySequence data)
memory = buffer.AsMemory(0, (int)data.Length);
}
- public void Initialize(ReadOnlyMemory data)
+ public void Initialize(ReadOnlyMemory data, bool holdReference)
{
ThrowIfUsing();
- buffer = null;
- memory = data;
+ if (holdReference)
+ {
+ buffer = null;
+ memory = data;
+ }
+ else
+ {
+ buffer = ArrayPool.Shared.Rent((int)data.Length);
+ data.CopyTo(buffer);
+ memory = buffer.AsMemory(0, (int)data.Length);
+ }
}
public void Uninitialize()
diff --git a/src/MagicOnion.Internal/StreamingHubPayloadPool.BuiltIn.cs b/src/MagicOnion.Internal/StreamingHubPayloadPool.BuiltIn.cs
index 8a8334abb..2245d3e88 100644
--- a/src/MagicOnion.Internal/StreamingHubPayloadPool.BuiltIn.cs
+++ b/src/MagicOnion.Internal/StreamingHubPayloadPool.BuiltIn.cs
@@ -105,10 +105,10 @@ public StreamingHubPayload RentOrCreate(ReadOnlySpan data)
#endif
}
- public StreamingHubPayload RentOrCreate(ReadOnlyMemory data)
+ public StreamingHubPayload RentOrCreate(ReadOnlyMemory data, bool holdReference)
{
var payload = pool.RentOrCreateCore();
- payload.Initialize(data);
+ payload.Initialize(data, holdReference);
#if DEBUG
return new StreamingHubPayload(payload);
#else
diff --git a/src/MagicOnion.Internal/StreamingHubPayloadPool.ObjectPool.cs b/src/MagicOnion.Internal/StreamingHubPayloadPool.ObjectPool.cs
index 62c85b124..1a0948825 100644
--- a/src/MagicOnion.Internal/StreamingHubPayloadPool.ObjectPool.cs
+++ b/src/MagicOnion.Internal/StreamingHubPayloadPool.ObjectPool.cs
@@ -34,10 +34,10 @@ public StreamingHubPayload RentOrCreate(ReadOnlySpan data)
#endif
}
- public StreamingHubPayload RentOrCreate(ReadOnlyMemory data)
+ public StreamingHubPayload RentOrCreate(ReadOnlyMemory data, bool holdReference)
{
var payload = pool.Get();
- payload.Initialize(data);
+ payload.Initialize(data, holdReference);
#if DEBUG
return new StreamingHubPayload(payload);
#else
diff --git a/src/MagicOnion.Internal/StreamingHubServerMessageReader.cs b/src/MagicOnion.Internal/StreamingHubServerMessageReader.cs
index b1d8326cf..ae03323b2 100644
--- a/src/MagicOnion.Internal/StreamingHubServerMessageReader.cs
+++ b/src/MagicOnion.Internal/StreamingHubServerMessageReader.cs
@@ -25,7 +25,8 @@ public StreamingHubMessageType ReadMessageType()
{
0x00 => StreamingHubMessageType.ClientResultResponse,
0x01 => StreamingHubMessageType.ClientResultResponseWithError,
- 0x7f => StreamingHubMessageType.HeartbeatResponse,
+ 0x7e => StreamingHubMessageType.ClientHeartbeat,
+ 0x7f => StreamingHubMessageType.ServerHeartbeatResponse,
var subType => throw new InvalidOperationException($"Unknown client response message: {subType}"),
},
_ => throw new InvalidOperationException($"Unknown message format: ArrayLength = {arrayLength}"),
@@ -75,5 +76,14 @@ public StreamingHubMessageType ReadMessageType()
return (clientResultMessageId, clientMethodId, statusCode, detail, message);
}
+
+ public ReadOnlyMemory ReadClientHeartbeat()
+ {
+ // [Nil, Nil, [SentAt(long)]]
+ reader.Skip(); // Dummy
+ reader.Skip(); // Dummy
+
+ return data.Slice((int)reader.Consumed);
+ }
}
}
diff --git a/src/MagicOnion.Server/Hubs/StreamingHub.cs b/src/MagicOnion.Server/Hubs/StreamingHub.cs
index 0b6da9f9c..f4a463cb9 100644
--- a/src/MagicOnion.Server/Hubs/StreamingHub.cs
+++ b/src/MagicOnion.Server/Hubs/StreamingHub.cs
@@ -1,3 +1,4 @@
+using System.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading.Channels;
@@ -5,6 +6,7 @@
using Cysharp.Runtime.Multicast.Remoting;
using Grpc.Core;
using MagicOnion.Internal;
+using MagicOnion.Internal.Buffers;
using MagicOnion.Server.Diagnostics;
using MagicOnion.Server.Internal;
using MessagePack;
@@ -224,11 +226,22 @@ ValueTask ProcessMessageAsync(StreamingHubPayload payload, UniqueHashDictionary<
}
return default;
}
- case StreamingHubMessageType.HeartbeatResponse:
+ case StreamingHubMessageType.ServerHeartbeatResponse:
{
heartbeatHandle.Ack();
return default;
}
+ case StreamingHubMessageType.ClientHeartbeat:
+ {
+ var heartbeatBody = reader.ReadClientHeartbeat();
+
+ using var bufferWriter = ArrayPoolBufferWriter.RentThreadStaticWriter();
+ StreamingHubMessageWriter.WriteClientHeartbeatMessageResponseHeader(bufferWriter);
+ bufferWriter.Write(heartbeatBody.Span);
+
+ StreamingServiceContext.QueueResponseStreamWrite(StreamingHubPayloadPool.Shared.RentOrCreate(bufferWriter.WrittenSpan));
+ return default;
+ }
default:
throw new InvalidOperationException($"Unknown MessageType: {messageType}");
}
diff --git a/src/MagicOnion.Server/Hubs/StreamingHubHeartbeatManager.cs b/src/MagicOnion.Server/Hubs/StreamingHubHeartbeatManager.cs
index d42853f6d..00ad58931 100644
--- a/src/MagicOnion.Server/Hubs/StreamingHubHeartbeatManager.cs
+++ b/src/MagicOnion.Server/Hubs/StreamingHubHeartbeatManager.cs
@@ -23,12 +23,16 @@ internal class StreamingHubHeartbeatHandle : IDisposable
public IStreamingServiceContext ServiceContext { get; }
public CancellationToken TimeoutToken => timeoutToken.Token;
- public StreamingHubHeartbeatHandle(IStreamingHubHeartbeatManager manager, IStreamingServiceContext serviceContext, TimeSpan timeoutDuration)
+ public StreamingHubHeartbeatHandle(IStreamingHubHeartbeatManager manager, IStreamingServiceContext serviceContext, TimeSpan timeoutDuration, TimeProvider timeProvider)
{
this.manager = manager;
this.ServiceContext = serviceContext;
this.timeoutDuration = timeoutDuration;
- this.timeoutToken = new CancellationTokenSource();
+ this.timeoutToken = new CancellationTokenSource(Timeout.InfiniteTimeSpan
+#if NET8_0_OR_GREATER
+ , timeProvider
+#endif
+ );
}
public void RestartTimeoutTimer()
@@ -59,7 +63,7 @@ internal class NopStreamingHubHeartbeatManager : IStreamingHubHeartbeatManager
NopStreamingHubHeartbeatManager() {}
public StreamingHubHeartbeatHandle Register(IStreamingServiceContext serviceContext)
- => new(this, serviceContext, Timeout.InfiniteTimeSpan);
+ => new(this, serviceContext, Timeout.InfiniteTimeSpan, TimeProvider.System);
public void Unregister(IStreamingServiceContext serviceContext) { }
public void Dispose() { }
}
@@ -72,23 +76,25 @@ internal class StreamingHubHeartbeatManager : IStreamingHubHeartbeatManager
readonly IStreamingHubHeartbeatMetadataProvider? heartbeatMetadataProvider;
readonly TimeSpan heartbeatInterval;
readonly TimeSpan timeoutDuration;
+ readonly TimeProvider timeProvider;
readonly ILogger logger;
PeriodicTimer? timer;
int registeredCount;
ConcurrentDictionary contexts = new();
- public StreamingHubHeartbeatManager(TimeSpan heartbeatInterval, TimeSpan timeoutDuration, IStreamingHubHeartbeatMetadataProvider? heartbeatMetadataProvider, ILogger logger)
+ public StreamingHubHeartbeatManager(TimeSpan heartbeatInterval, TimeSpan timeoutDuration, IStreamingHubHeartbeatMetadataProvider? heartbeatMetadataProvider, TimeProvider timeProvider, ILogger logger)
{
this.heartbeatInterval = heartbeatInterval;
this.timeoutDuration = timeoutDuration;
this.heartbeatMetadataProvider = heartbeatMetadataProvider;
+ this.timeProvider = timeProvider;
this.logger = logger;
}
public StreamingHubHeartbeatHandle Register(IStreamingServiceContext serviceContext)
{
- var handle = new StreamingHubHeartbeatHandle(this, serviceContext, timeoutDuration);
+ var handle = new StreamingHubHeartbeatHandle(this, serviceContext, timeoutDuration, timeProvider);
if (contexts.TryAdd(serviceContext.ContextId, handle))
{
if (Interlocked.Increment(ref registeredCount) == 1)
@@ -97,7 +103,11 @@ public StreamingHubHeartbeatHandle Register(IStreamingServiceContext();
while (await runningTimer.WaitForNextTickAsync())
{
- StreamingHubMessageWriter.WriteHeartbeatMessageForServerToClientHeader(writer);
+ StreamingHubMessageWriter.WriteServerHeartbeatMessageHeader(writer);
if (!(heartbeatMetadataProvider?.TryWriteMetadata(writer) ?? false))
{
writer.Write(Nil);
diff --git a/src/MagicOnion.Server/MagicOnionEngine.cs b/src/MagicOnion.Server/MagicOnionEngine.cs
index 871ad4e0f..f168a7e8a 100644
--- a/src/MagicOnion.Server/MagicOnionEngine.cs
+++ b/src/MagicOnion.Server/MagicOnionEngine.cs
@@ -254,6 +254,7 @@ public static MagicOnionServiceDefinition BuildServerServiceDefinition(IServiceP
heartbeatInterval.Value,
heartbeatTimeout ?? Timeout.InfiniteTimeSpan,
heartbeatMetadataProvider ?? serviceProvider.GetService(),
+ TimeProvider.System,
serviceProvider.GetRequiredService>()
);
}
diff --git a/tests/MagicOnion.Client.Tests/MagicOnion.Client.Tests.csproj b/tests/MagicOnion.Client.Tests/MagicOnion.Client.Tests.csproj
index 13484171c..957b2448f 100644
--- a/tests/MagicOnion.Client.Tests/MagicOnion.Client.Tests.csproj
+++ b/tests/MagicOnion.Client.Tests/MagicOnion.Client.Tests.csproj
@@ -13,6 +13,7 @@
+
diff --git a/tests/MagicOnion.Client.Tests/StreamingHubClientHeartbeatManagerTest.cs b/tests/MagicOnion.Client.Tests/StreamingHubClientHeartbeatManagerTest.cs
new file mode 100644
index 000000000..0f88a74c7
--- /dev/null
+++ b/tests/MagicOnion.Client.Tests/StreamingHubClientHeartbeatManagerTest.cs
@@ -0,0 +1,208 @@
+using System.Buffers;
+using System.Diagnostics;
+using System.Threading.Channels;
+using MagicOnion.Internal;
+using Microsoft.Extensions.Time.Testing;
+
+namespace MagicOnion.Client.Tests;
+
+public class StreamingHubClientHeartbeatManagerTest
+{
+ [Fact]
+ public async Task Interval_TimeoutDisabled()
+ {
+ // Arrange
+ var channel = Channel.CreateUnbounded();
+ var interval = TimeSpan.FromSeconds(1);
+ var timeout = Timeout.InfiniteTimeSpan;
+ var serverHeartbeatReceived = new List>();
+ var clientHeartbeatResponseReceived = new List();
+ var origin = new DateTimeOffset(2024, 7, 1, 0, 0, 0, TimeSpan.Zero);
+ var timeProvider = new FakeTimeProvider(origin);
+ using var manager = new StreamingHubClientHeartbeatManager(
+ channel.Writer,
+ interval,
+ timeout,
+ onServerHeartbeatReceived: x => serverHeartbeatReceived.Add(x),
+ onClientHeartbeatResponseReceived: x => clientHeartbeatResponseReceived.Add(x),
+ synchronizationContext: null,
+ shutdownToken: CancellationToken.None,
+ timeProvider
+ );
+
+ // Act
+ manager.StartClientHeartbeatLoop();
+ timeProvider.Advance(TimeSpan.FromSeconds(1));
+ await Task.Delay(50);
+ timeProvider.Advance(TimeSpan.FromSeconds(1));
+ await Task.Delay(50);
+ timeProvider.Advance(TimeSpan.FromSeconds(1));
+ await Task.Delay(50);
+
+ // Assert
+ Assert.True(channel.Reader.TryRead(out var heartbeat1));
+ Assert.Equal((byte[])[0x94 /* Array(4) */, 0x7e /* 0x7e(127) */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0x91 /* Array(1) */, .. ToMessagePackBytes(origin.AddSeconds(1))], heartbeat1.Memory.ToArray());
+ Assert.True(channel.Reader.TryRead(out var heartbeat2));
+ Assert.Equal((byte[])[0x94 /* Array(4) */, 0x7e /* 0x7e(127) */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0x91 /* Array(1) */, .. ToMessagePackBytes(origin.AddSeconds(2))], heartbeat2.Memory.ToArray());
+ Assert.True(channel.Reader.TryRead(out var heartbeat3));
+ Assert.Equal((byte[])[0x94 /* Array(4) */, 0x7e /* 0x7e(127) */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0x91 /* Array(1) */, .. ToMessagePackBytes(origin.AddSeconds(3))], heartbeat3.Memory.ToArray());
+
+ Assert.False(manager.TimeoutToken.IsCancellationRequested);
+ }
+
+ [Fact]
+ public async Task Elapsed_RoundTripTime()
+ {
+ // Arrange
+ var channel = Channel.CreateUnbounded();
+ var interval = TimeSpan.FromSeconds(1);
+ var timeout = Timeout.InfiniteTimeSpan;
+ var serverHeartbeatReceived = new List>();
+ var clientHeartbeatResponseReceived = new List();
+ var origin = new DateTimeOffset(2024, 7, 1, 0, 0, 0, TimeSpan.Zero);
+ var timeProvider = new FakeTimeProvider(origin);
+ using var manager = new StreamingHubClientHeartbeatManager(
+ channel.Writer,
+ interval,
+ timeout,
+ onServerHeartbeatReceived: x => serverHeartbeatReceived.Add(x),
+ onClientHeartbeatResponseReceived: x => clientHeartbeatResponseReceived.Add(x),
+ synchronizationContext: null,
+ shutdownToken: CancellationToken.None,
+ timeProvider
+ );
+
+ // Act
+ manager.StartClientHeartbeatLoop();
+ timeProvider.Advance(TimeSpan.FromMilliseconds(1000)); // Send
+ await Task.Delay(50);
+ timeProvider.Advance(TimeSpan.FromMilliseconds(100));
+ await Task.Delay(50);
+ manager.ProcessClientHeartbeatResponse(StreamingHubPayloadPool.Shared.RentOrCreate([0x95 /* Array(5) */, 0x7e /* 0x7e(127) */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0x91 /* Array(1) */, .. ToMessagePackBytes(timeProvider.GetUtcNow().AddMilliseconds(-100))]));
+
+ timeProvider.Advance(TimeSpan.FromMilliseconds(900)); // Send
+ await Task.Delay(50);
+ timeProvider.Advance(TimeSpan.FromMilliseconds(100));
+ await Task.Delay(50);
+ manager.ProcessClientHeartbeatResponse(StreamingHubPayloadPool.Shared.RentOrCreate([0x95 /* Array(5) */, 0x7e /* 0x7e(127) */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0x91 /* Array(1) */, .. ToMessagePackBytes(timeProvider.GetUtcNow().AddMilliseconds(-100))]));
+
+ timeProvider.Advance(TimeSpan.FromMilliseconds(900)); // Send
+ await Task.Delay(50);
+ timeProvider.Advance(TimeSpan.FromMilliseconds(100));
+ await Task.Delay(50);
+ manager.ProcessClientHeartbeatResponse(StreamingHubPayloadPool.Shared.RentOrCreate([0x95 /* Array(5) */, 0x7e /* 0x7e(127) */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0x91 /* Array(1) */, .. ToMessagePackBytes(timeProvider.GetUtcNow().AddMilliseconds(-100))]));
+
+ await Task.Delay(50);
+
+ // Assert
+ Assert.Equal(3, clientHeartbeatResponseReceived.Count);
+ Assert.True(channel.Reader.TryRead(out var heartbeat1));
+ Assert.Equal((byte[])[0x94 /* Array(4) */, 0x7e /* 0x7e(127) */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0x91 /* Array(1) */, .. ToMessagePackBytes(origin.AddSeconds(1))], heartbeat1.Memory.ToArray());
+ Assert.Equal(TimeSpan.FromMilliseconds(100), clientHeartbeatResponseReceived[0].RoundTripTime);
+ Assert.True(channel.Reader.TryRead(out var heartbeat2));
+ Assert.Equal((byte[])[0x94 /* Array(4) */, 0x7e /* 0x7e(127) */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0x91 /* Array(1) */, .. ToMessagePackBytes(origin.AddSeconds(2))], heartbeat2.Memory.ToArray());
+ Assert.Equal(TimeSpan.FromMilliseconds(100), clientHeartbeatResponseReceived[1].RoundTripTime);
+ Assert.True(channel.Reader.TryRead(out var heartbeat3));
+ Assert.Equal((byte[])[0x94 /* Array(4) */, 0x7e /* 0x7e(127) */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0x91 /* Array(1) */, .. ToMessagePackBytes(origin.AddSeconds(3))], heartbeat3.Memory.ToArray());
+ Assert.Equal(TimeSpan.FromMilliseconds(100), clientHeartbeatResponseReceived[2].RoundTripTime);
+
+
+ Assert.False(manager.TimeoutToken.IsCancellationRequested);
+ }
+
+ [Fact]
+ public async Task Timeout_Not_Responding()
+ {
+ // Arrange
+ var channel = Channel.CreateUnbounded();
+ var interval = TimeSpan.FromSeconds(1);
+ var timeout = TimeSpan.FromMilliseconds(500);
+ var serverHeartbeatReceived = new List>();
+ var clientHeartbeatResponseReceived = new List();
+ var origin = new DateTimeOffset(2024, 7, 1, 0, 0, 0, TimeSpan.Zero);
+ var timeProvider = new FakeTimeProvider(origin);
+ using var manager = new StreamingHubClientHeartbeatManager(
+ channel.Writer,
+ interval,
+ timeout,
+ onServerHeartbeatReceived: x => serverHeartbeatReceived.Add(x),
+ onClientHeartbeatResponseReceived: x => clientHeartbeatResponseReceived.Add(x),
+ synchronizationContext: null,
+ shutdownToken: CancellationToken.None,
+ timeProvider
+ );
+
+ // Act
+ manager.StartClientHeartbeatLoop();
+ timeProvider.Advance(TimeSpan.FromSeconds(1));
+ await Task.Delay(50);
+ timeProvider.Advance(TimeSpan.FromSeconds(1));
+ await Task.Delay(50);
+
+ // Assert
+ Assert.True(channel.Reader.TryRead(out var heartbeat1));
+ Assert.Equal((byte[])[0x94 /* Array(4) */, 0x7e /* 0x7e(127) */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0x91 /* Array(1) */, .. (ToMessagePackBytes(origin.AddSeconds(1)))], heartbeat1.Memory.ToArray());
+ Assert.False(channel.Reader.TryRead(out var heartbeat2));
+
+ Assert.True(manager.TimeoutToken.IsCancellationRequested);
+ }
+
+ [Fact]
+ public async Task Timeout_Keep()
+ {
+ // Arrange
+ var channel = Channel.CreateUnbounded();
+ var interval = TimeSpan.FromSeconds(1);
+ var timeout = TimeSpan.FromMilliseconds(500);
+ var serverHeartbeatReceived = new List>();
+ var clientHeartbeatResponseReceived = new List();
+ var origin = new DateTimeOffset(2024, 7, 1, 0, 0, 0, TimeSpan.Zero);
+ var timeProvider = new FakeTimeProvider(origin);
+ using var manager = new StreamingHubClientHeartbeatManager(
+ channel.Writer,
+ interval,
+ timeout,
+ onServerHeartbeatReceived: x => serverHeartbeatReceived.Add(x),
+ onClientHeartbeatResponseReceived: x => clientHeartbeatResponseReceived.Add(x),
+ synchronizationContext: null,
+ shutdownToken: CancellationToken.None,
+ timeProvider
+ );
+
+ // Act && Assert
+ manager.StartClientHeartbeatLoop();
+
+ timeProvider.Advance(TimeSpan.FromSeconds(1)); // Send a client heartbeat message
+ await Task.Delay(50);
+
+ Assert.True(channel.Reader.TryRead(out var heartbeat1));
+
+ timeProvider.Advance(TimeSpan.FromMilliseconds(250));
+ await Task.Delay(50);
+ Assert.False(manager.TimeoutToken.IsCancellationRequested);
+
+ // Received a response message from the server.
+ manager.ProcessClientHeartbeatResponse(StreamingHubPayloadPool.Shared.RentOrCreate([0x95 /* Array(5) */, 0x7e /* 0x7e(127) */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0x91 /* Array(1) */, .. ToMessagePackBytes(origin.AddSeconds(1))]));
+
+ timeProvider.Advance(TimeSpan.FromMilliseconds(250));
+ await Task.Delay(50);
+ Assert.False(manager.TimeoutToken.IsCancellationRequested);
+
+ timeProvider.Advance(TimeSpan.FromMilliseconds(500));
+ await Task.Delay(50);
+ Assert.False(manager.TimeoutToken.IsCancellationRequested);
+
+ Assert.True(channel.Reader.TryRead(out var heartbeat2));
+ }
+
+ static byte[] ToMessagePackBytes(DateTimeOffset dt)
+ {
+ var ms = dt.ToUnixTimeMilliseconds();
+
+ var arrayBufferWriter = new ArrayBufferWriter();
+ var writer = new MessagePackWriter(arrayBufferWriter);
+ writer.Write(ms);
+ writer.Flush();
+ return arrayBufferWriter.WrittenMemory.ToArray();
+ }
+}
diff --git a/tests/MagicOnion.Client.Tests/StreamingHubTest.cs b/tests/MagicOnion.Client.Tests/StreamingHubTest.cs
index 38aa490bb..03e1c8f0a 100644
--- a/tests/MagicOnion.Client.Tests/StreamingHubTest.cs
+++ b/tests/MagicOnion.Client.Tests/StreamingHubTest.cs
@@ -1,4 +1,6 @@
using MagicOnion.Client.DynamicClient;
+using Microsoft.Extensions.Time.Testing;
+using System.Buffers;
namespace MagicOnion.Client.Tests;
@@ -436,14 +438,22 @@ public async Task Void_Parameter_Many()
public async Task Heartbeat_Interval()
{
// Arrange
+ var origin = new DateTimeOffset(2024, 7, 1, 0, 0, 0, TimeSpan.Zero);
+ var timeProvider = new FakeTimeProvider(origin);
var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var helper = new StreamingHubClientTestHelper(factoryProvider: DynamicStreamingHubClientFactoryProvider.Instance);
- var options = StreamingHubClientOptions.CreateWithDefault().WithHeartbeatInterval(TimeSpan.FromMilliseconds(100));
+ var options = StreamingHubClientOptions.CreateWithDefault().WithClientHeartbeatInterval(TimeSpan.FromMilliseconds(100)).WithTimeProvider(timeProvider);
var client = await helper.ConnectAsync(options, timeout.Token);
+ byte[] sentAt = [0xcf, 0x00, 0x00, 0x01, 0x90, 0x6b, 0x97, 0x5c, 0x00]; // MessagePackWriter.Write(timeProvider.GetUtcNow().ToUnixTimeMilliseconds());
// Act
var t = client.Parameter_One(1234);
- await Task.Delay(300);
+ timeProvider.Advance(TimeSpan.FromMilliseconds(100));
+ await Task.Delay(100); // Wait for processing queue.
+ timeProvider.Advance(TimeSpan.FromMilliseconds(100));
+ await Task.Delay(100); // Wait for processing queue.
+ timeProvider.Advance(TimeSpan.FromMilliseconds(100));
+ await Task.Delay(100); // Wait for processing queue.
// Assert
var (messageId, methodId, requestBody) = await helper.ReadRequestAsync();
@@ -452,9 +462,20 @@ public async Task Heartbeat_Interval()
var request1 = await helper.ReadRequestRawAsync();
var request2 = await helper.ReadRequestRawAsync();
var request3 = await helper.ReadRequestRawAsync();
- Assert.Equal([0x94, 0x7f, 0xc0, 0xc0, 0xc0], request1.ToArray());
- Assert.Equal([0x94, 0x7f, 0xc0, 0xc0, 0xc0], request2.ToArray());
- Assert.Equal([0x94, 0x7f, 0xc0, 0xc0, 0xc0], request3.ToArray());
+ Assert.Equal((byte[])[0x94 /* Array(4) */, 0x7e /* 0x7e(127) */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0x91 /* Array(1) */, .. (ToMessagePackBytes(origin.AddMilliseconds(100)))], request1.ToArray());
+ Assert.Equal((byte[])[0x94 /* Array(4) */, 0x7e /* 0x7e(127) */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0x91 /* Array(1) */, .. (ToMessagePackBytes(origin.AddMilliseconds(200)))], request2.ToArray());
+ Assert.Equal((byte[])[0x94 /* Array(4) */, 0x7e /* 0x7e(127) */, 0xc0 /* Nil */, 0xc0 /* Nil */, 0x91 /* Array(1) */, .. (ToMessagePackBytes(origin.AddMilliseconds(300)))], request3.ToArray());
+
+ static byte[] ToMessagePackBytes(DateTimeOffset dt)
+ {
+ var ms = dt.ToUnixTimeMilliseconds();
+
+ var arrayBufferWriter = new ArrayBufferWriter();
+ var writer = new MessagePackWriter(arrayBufferWriter);
+ writer.Write(ms);
+ writer.Flush();
+ return arrayBufferWriter.WrittenMemory.ToArray();
+ }
}
[Fact]
@@ -463,7 +484,7 @@ public async Task Heartbeat_Respond()
// Arrange
var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var helper = new StreamingHubClientTestHelper(factoryProvider: DynamicStreamingHubClientFactoryProvider.Instance);
- var options = StreamingHubClientOptions.CreateWithDefault().WithHeartbeatInterval(Timeout.InfiniteTimeSpan); // Disable Heartbeat timer.
+ var options = StreamingHubClientOptions.CreateWithDefault().WithClientHeartbeatInterval(Timeout.InfiniteTimeSpan); // Disable Heartbeat timer.
var client = await helper.ConnectAsync(options, timeout.Token);
// Act
@@ -487,8 +508,8 @@ public async Task Heartbeat_Extra()
var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var helper = new StreamingHubClientTestHelper(factoryProvider: DynamicStreamingHubClientFactoryProvider.Instance);
var options = StreamingHubClientOptions.CreateWithDefault()
- .WithHeartbeatReceived(x => received = x.ToArray())
- .WithHeartbeatInterval(Timeout.InfiniteTimeSpan); // Disable Heartbeat timer.
+ .WithServerHeartbeatReceived(x => received = x.ToArray())
+ .WithClientHeartbeatInterval(Timeout.InfiniteTimeSpan); // Disable Heartbeat timer.
var client = await helper.ConnectAsync(options, timeout.Token);
// Act
diff --git a/tests/MagicOnion.Server.Tests/MagicOnion.Server.Tests.csproj b/tests/MagicOnion.Server.Tests/MagicOnion.Server.Tests.csproj
index 5bdca88a7..f67f8e07b 100644
--- a/tests/MagicOnion.Server.Tests/MagicOnion.Server.Tests.csproj
+++ b/tests/MagicOnion.Server.Tests/MagicOnion.Server.Tests.csproj
@@ -1,4 +1,4 @@
-
+
net8.0
@@ -17,6 +17,7 @@
+
diff --git a/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubClientHeartbeatResponseTest.cs b/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubClientHeartbeatResponseTest.cs
new file mode 100644
index 000000000..8b860b051
--- /dev/null
+++ b/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubClientHeartbeatResponseTest.cs
@@ -0,0 +1,48 @@
+using MagicOnion.Client;
+using MagicOnion.Server.Hubs;
+using NSubstitute;
+using static MagicOnion.Server.Tests.StreamingHubHeartbeat.StreamingHubClientHeartbeatResponseTest;
+
+namespace MagicOnion.Server.Tests.StreamingHubHeartbeat;
+
+public class StreamingHubClientHeartbeatResponseTest(StreamingHubClientHeartbeatResponseTestServerFixture fixture) : IClassFixture
+{
+ protected ServerFixture Fixture { get; } = fixture;
+
+ public class StreamingHubClientHeartbeatResponseTestServerFixture : ServerFixture<
+ StreamingHubClientHeartbeatResponseTestHub
+ >
+ {
+ protected override void ConfigureMagicOnion(MagicOnionOptions options)
+ {
+ options.EnableStreamingHubHeartbeat = false; // Server Heartbeat is disabled on this test.
+ }
+ }
+
+ [Fact]
+ public async Task Interval()
+ {
+ // Arrange
+ var receiver = Substitute.For();
+ var receivedHeartbeatEvents = new List();
+ var options = StreamingHubClientOptions.CreateWithDefault()
+ .WithClientHeartbeatInterval(TimeSpan.FromMilliseconds(100))
+ .WithClientHeartbeatResponseReceived(x => receivedHeartbeatEvents.Add(x));
+
+ // Act
+ var client = await Fixture.CreateStreamingHubClientAsync(receiver, options);
+ await Task.Delay(250);
+ await client.DisposeAsync();
+
+ // Assert
+ Assert.Equal(2, receivedHeartbeatEvents.Count);
+ }
+
+}
+
+public interface IStreamingHubClientHeartbeatResponseTestHub : IStreamingHub;
+public interface IStreamingHubClientHeartbeatResponseTestHubReceiver;
+
+public class StreamingHubClientHeartbeatResponseTestHub : StreamingHubBase, IStreamingHubClientHeartbeatResponseTestHub
+{
+}
diff --git a/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubHeartbeatManagerTest.cs b/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubHeartbeatManagerTest.cs
index 100abd5bd..70cf43429 100644
--- a/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubHeartbeatManagerTest.cs
+++ b/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubHeartbeatManagerTest.cs
@@ -1,3 +1,4 @@
+using System;
using System.Buffers;
using Grpc.Core;
using MagicOnion.Internal;
@@ -5,6 +6,7 @@
using MagicOnion.Server.Hubs;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Testing;
+using Microsoft.Extensions.Time.Testing;
namespace MagicOnion.Server.Tests.StreamingHubHeartbeat;
@@ -18,7 +20,8 @@ public void Register()
{
// Arrange
var logger = new FakeLogger();
- var manager = new StreamingHubHeartbeatManager(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan, null, logger);
+ var timeProvider = new FakeTimeProvider();
+ var manager = new StreamingHubHeartbeatManager(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan, null, timeProvider, logger);
var context = CreateFakeStreamingServiceContext();
// Act
@@ -35,7 +38,8 @@ public void Handle_Dispose()
{
// Arrange
var logger = new FakeLogger();
- var manager = new StreamingHubHeartbeatManager(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan, null, logger);
+ var timeProvider = new FakeTimeProvider();
+ var manager = new StreamingHubHeartbeatManager(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan, null, timeProvider, logger);
var context = CreateFakeStreamingServiceContext();
var handle = manager.Register(context);
@@ -48,7 +52,8 @@ public async Task Interval_Disable_Timeout()
{
// Arrange
var logger = new FakeLogger();
- var manager = new StreamingHubHeartbeatManager(TimeSpan.FromMilliseconds(100), Timeout.InfiniteTimeSpan, null, logger);
+ var timeProvider = new FakeTimeProvider();
+ var manager = new StreamingHubHeartbeatManager(TimeSpan.FromMilliseconds(100), Timeout.InfiniteTimeSpan, null, timeProvider, logger);
var context1 = CreateFakeStreamingServiceContext();
var context2 = CreateFakeStreamingServiceContext();
var context3 = CreateFakeStreamingServiceContext();
@@ -57,7 +62,12 @@ public async Task Interval_Disable_Timeout()
using var handle1 = manager.Register(context1);
using var handle2 = manager.Register(context2);
using var handle3 = manager.Register(context3);
- await Task.Delay(310);
+ timeProvider.Advance(TimeSpan.FromMilliseconds(100));
+ await Task.Delay(50);
+ timeProvider.Advance(TimeSpan.FromMilliseconds(100));
+ await Task.Delay(50);
+ timeProvider.Advance(TimeSpan.FromMilliseconds(100));
+ await Task.Delay(50);
// Assert
Assert.Equal(3, context1.Responses.Count);
@@ -80,7 +90,8 @@ public async Task Interval_Keep()
// Arrange
var collector = FakeLogCollector.Create(new FakeLogCollectorOptions());
var logger = new FakeLogger(collector);
- var manager = new StreamingHubHeartbeatManager(TimeSpan.FromMilliseconds(300), TimeSpan.FromMilliseconds(200), null, logger);
+ var timeProvider = new FakeTimeProvider();
+ var manager = new StreamingHubHeartbeatManager(TimeSpan.FromMilliseconds(300), TimeSpan.FromMilliseconds(200), null, timeProvider, logger);
var context1 = CreateFakeStreamingServiceContext();
var context2 = CreateFakeStreamingServiceContext();
var context3 = CreateFakeStreamingServiceContext();
@@ -89,7 +100,8 @@ public async Task Interval_Keep()
using var handle1 = manager.Register(context1);
using var handle2 = manager.Register(context2);
using var handle3 = manager.Register(context3);
- await Task.Delay(350);
+ timeProvider.Advance(TimeSpan.FromMilliseconds(350));
+ await Task.Delay(50);
var isCanceled1 = handle1.TimeoutToken.IsCancellationRequested;
var isCanceled2 = handle2.TimeoutToken.IsCancellationRequested;
var isCanceled3 = handle3.TimeoutToken.IsCancellationRequested;
@@ -97,7 +109,8 @@ public async Task Interval_Keep()
handle1.Ack();
handle2.Ack();
handle3.Ack();
- await Task.Delay(250);
+ timeProvider.Advance(TimeSpan.FromMilliseconds(250));
+ await Task.Delay(50);
// Assert
Assert.False(isCanceled1);
@@ -114,7 +127,8 @@ public async Task Interval_With_Timeout()
// Arrange
var collector = FakeLogCollector.Create(new FakeLogCollectorOptions());
var logger = new FakeLogger(collector);
- var manager = new StreamingHubHeartbeatManager(TimeSpan.FromMilliseconds(300), TimeSpan.FromMilliseconds(200), null, logger);
+ var timeProvider = new FakeTimeProvider();
+ var manager = new StreamingHubHeartbeatManager(TimeSpan.FromMilliseconds(300), TimeSpan.FromMilliseconds(200), null, timeProvider, logger);
var context1 = CreateFakeStreamingServiceContext();
var context2 = CreateFakeStreamingServiceContext();
var context3 = CreateFakeStreamingServiceContext();
@@ -123,11 +137,13 @@ public async Task Interval_With_Timeout()
using var handle1 = manager.Register(context1);
using var handle2 = manager.Register(context2);
using var handle3 = manager.Register(context3);
- await Task.Delay(350);
+ timeProvider.Advance(TimeSpan.FromMilliseconds(350));
+ await Task.Delay(50);
var isCanceled1 = handle1.TimeoutToken.IsCancellationRequested;
var isCanceled2 = handle2.TimeoutToken.IsCancellationRequested;
var isCanceled3 = handle3.TimeoutToken.IsCancellationRequested;
- await Task.Delay(250); // No responses from clients and timeouts are reached.
+ timeProvider.Advance(TimeSpan.FromMilliseconds(250)); // No responses from clients and timeouts are reached.
+ await Task.Delay(50);
// Assert
Assert.False(isCanceled1);
@@ -143,7 +159,8 @@ public async Task Interval_Stop_After_HandleDisposed()
{
// Arrange
var logger = new FakeLogger();
- var manager = new StreamingHubHeartbeatManager(TimeSpan.FromMilliseconds(100), Timeout.InfiniteTimeSpan, null, logger);
+ var timeProvider = new FakeTimeProvider();
+ var manager = new StreamingHubHeartbeatManager(TimeSpan.FromMilliseconds(100), Timeout.InfiniteTimeSpan, null, timeProvider, logger);
var context1 = CreateFakeStreamingServiceContext();
var context2 = CreateFakeStreamingServiceContext();
var context3 = CreateFakeStreamingServiceContext();
@@ -152,11 +169,21 @@ public async Task Interval_Stop_After_HandleDisposed()
var handle1 = manager.Register(context1);
var handle2 = manager.Register(context2);
var handle3 = manager.Register(context3);
- await Task.Delay(310);
+ timeProvider.Advance(TimeSpan.FromMilliseconds(100));
+ await Task.Delay(50);
+ timeProvider.Advance(TimeSpan.FromMilliseconds(100));
+ await Task.Delay(50);
+ timeProvider.Advance(TimeSpan.FromMilliseconds(100));
+ await Task.Delay(50);
handle1.Dispose();
handle2.Dispose();
handle3.Dispose();
- await Task.Delay(300);
+ timeProvider.Advance(TimeSpan.FromMilliseconds(100));
+ await Task.Delay(50);
+ timeProvider.Advance(TimeSpan.FromMilliseconds(100));
+ await Task.Delay(50);
+ timeProvider.Advance(TimeSpan.FromMilliseconds(100));
+ await Task.Delay(50);
// Assert
Assert.Equal(3, context1.Responses.Count);
@@ -178,7 +205,8 @@ public async Task CustomMetadataProvider()
{
// Arrange
var logger = new FakeLogger();
- var manager = new StreamingHubHeartbeatManager(TimeSpan.FromMilliseconds(100), Timeout.InfiniteTimeSpan, new CustomHeartbeatMetadataProvider(), logger);
+ var timeProvider = new FakeTimeProvider();
+ var manager = new StreamingHubHeartbeatManager(TimeSpan.FromMilliseconds(100), Timeout.InfiniteTimeSpan, new CustomHeartbeatMetadataProvider(), timeProvider, logger);
var context1 = CreateFakeStreamingServiceContext();
var context2 = CreateFakeStreamingServiceContext();
var context3 = CreateFakeStreamingServiceContext();
@@ -188,7 +216,12 @@ public async Task CustomMetadataProvider()
using var handle1 = manager.Register(context1);
using var handle2 = manager.Register(context2);
using var handle3 = manager.Register(context3);
- await Task.Delay(310);
+ timeProvider.Advance(TimeSpan.FromMilliseconds(100));
+ await Task.Delay(50);
+ timeProvider.Advance(TimeSpan.FromMilliseconds(100));
+ await Task.Delay(50);
+ timeProvider.Advance(TimeSpan.FromMilliseconds(100));
+ await Task.Delay(50);
// Assert
Assert.Equal(3, context1.Responses.Count);
diff --git a/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubHeartbeatTest.cs b/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubHeartbeatTest.cs
deleted file mode 100644
index 4525fe28a..000000000
--- a/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubHeartbeatTest.cs
+++ /dev/null
@@ -1,218 +0,0 @@
-using System.Collections.Concurrent;
-using MagicOnion.Client;
-using MagicOnion.Server.Hubs;
-using Microsoft.Extensions.DependencyInjection;
-using NSubstitute;
-
-namespace MagicOnion.Server.Tests.StreamingHubHeartbeat;
-
-public abstract class StreamingHubHeartbeatTestBase
-{
- protected ServerFixture Fixture { get; }
-
- public StreamingHubHeartbeatTestBase(ServerFixture fixture)
- {
- this.Fixture = fixture;
- }
-
- [Fact]
- public async Task EnableByAttribute()
- {
- // Arrange
- var receiver = Substitute.For();
- var receivedHeartbeatMetadata = new List();
- var options = StreamingHubClientOptions.CreateWithDefault().WithHeartbeatReceived(x => receivedHeartbeatMetadata.Add(x.ToArray()));
-
- // Act
- var client = await Fixture.CreateStreamingHubClientAsync(receiver, options);
- await Task.Delay(650);
- await client.DisposeAsync();
-
- // Assert
- Assert.Equal(2, receivedHeartbeatMetadata.Count); // The client must receive a heartbeat every 300ms from the server.
- }
-
- [Fact]
- public async Task DisableByAttribute()
- {
- // Arrange
- var receiver = Substitute.For();
- var receivedHeartbeatMetadata = new List();
- var options = StreamingHubClientOptions.CreateWithDefault().WithHeartbeatReceived(x => receivedHeartbeatMetadata.Add(x.ToArray()));
-
- // Act
- var client = await Fixture.CreateStreamingHubClientAsync(receiver, options);
- await Task.Delay(650);
- await client.DisposeAsync();
-
- // Assert
- Assert.Empty(receivedHeartbeatMetadata);
- }
-
- [Fact]
- public async Task Override_Interval()
- {
- // Arrange
- var receiver = Substitute.For();
- var receivedHeartbeatMetadata = new List();
- var options = StreamingHubClientOptions.CreateWithDefault().WithHeartbeatReceived(x => receivedHeartbeatMetadata.Add(x.ToArray()));
-
- // Act
- var client = await Fixture.CreateStreamingHubClientAsync(receiver, options);
- await Task.Delay(650);
- await client.DisposeAsync();
-
- // Assert
- Assert.Single(receivedHeartbeatMetadata); // The client must receive a heartbeat every 500ms from the server.
- }
-
- [Fact]
- public async Task Timeout()
- {
- // Arrange
- var heartbeatReceived = new TaskCompletionSource();
- var receiver = Substitute.For();
- var options = StreamingHubClientOptions.CreateWithDefault().WithHeartbeatReceived(x =>
- {
- heartbeatReceived.SetResult();
- Thread.Sleep(200); // Block consuming message loop.
- });
-
- // We need to consume message inline. Avoid post continuations to the synchronization context.
- SynchronizationContext.SetSynchronizationContext(null);
-
- // Act
- var client = await Fixture.CreateStreamingHubClientAsync(receiver, options);
-
- // Wait for receiving a heartbeat from the server.
- // The client must receive a heartbeat every 200ms from the server.
- await heartbeatReceived.Task.WaitAsync(TimeSpan.FromSeconds(1));
-
- // Timeout at 100 ms after receiving a heartbeat.
- await Task.Delay(500);
-
- // Assert
- Assert.True((bool)Fixture.Items.GetValueOrDefault("Disconnected"));
- Assert.True(client.WaitForDisconnect().IsCompleted);
- }
-}
-
-
-public class StreamingHubHeartbeatTest_DisabledByDefault : StreamingHubHeartbeatTestBase, IClassFixture
-{
- public StreamingHubHeartbeatTest_DisabledByDefault(StreamingHubHeartbeatTestServerFixture fixture)
- : base(fixture)
- {
- }
-
- public class StreamingHubHeartbeatTestServerFixture : ServerFixture<
- StreamingHubHeartbeatTestHub,
- StreamingHubHeartbeatTestHub_EnableByAttribute,
- StreamingHubHeartbeatTestHub_DisableByAttribute,
- StreamingHubHeartbeatTestHub_CustomIntervalAndTimeout,
- StreamingHubHeartbeatTestHub_TimeoutBehavior
- >
- {
- protected override void ConfigureMagicOnion(MagicOnionOptions options)
- {
- options.StreamingHubHeartbeatInterval = TimeSpan.FromMilliseconds(300);
- options.StreamingHubHeartbeatTimeout = TimeSpan.FromMilliseconds(200);
- options.EnableStreamingHubHeartbeat = false; // Disabled by default.
- }
- }
-
- [Fact]
- public async Task Default_Disable()
- {
- // Arrange
- var receiver = Substitute.For();
- var receivedHeartbeatMetadata = new List();
- var options = StreamingHubClientOptions.CreateWithDefault().WithHeartbeatReceived(x => receivedHeartbeatMetadata.Add(x.ToArray()));
-
- // Act
- var client = await Fixture.CreateStreamingHubClientAsync(receiver, options);
- await Task.Delay(650);
- await client.DisposeAsync();
-
- // Assert
- Assert.Empty(receivedHeartbeatMetadata);
- }
-}
-
-
-public class StreamingHubHeartbeatTest_EnabledByDefault : StreamingHubHeartbeatTestBase, IClassFixture
-{
- public StreamingHubHeartbeatTest_EnabledByDefault(StreamingHubHeartbeatTestServerFixture fixture)
- : base(fixture)
- {
- }
-
- public class StreamingHubHeartbeatTestServerFixture : ServerFixture<
- StreamingHubHeartbeatTestHub,
- StreamingHubHeartbeatTestHub_EnableByAttribute,
- StreamingHubHeartbeatTestHub_DisableByAttribute,
- StreamingHubHeartbeatTestHub_CustomIntervalAndTimeout,
- StreamingHubHeartbeatTestHub_TimeoutBehavior
- >
- {
- protected override void ConfigureMagicOnion(MagicOnionOptions options)
- {
- options.StreamingHubHeartbeatInterval = TimeSpan.FromMilliseconds(300);
- options.StreamingHubHeartbeatTimeout = TimeSpan.FromMilliseconds(200);
- options.EnableStreamingHubHeartbeat = true; // Enabled by default.
- }
- }
-
- [Fact]
- public async Task Default_Enable()
- {
- // Arrange
- var receiver = Substitute.For();
- var receivedHeartbeatMetadata = new List();
- var options = StreamingHubClientOptions.CreateWithDefault().WithHeartbeatReceived(x => receivedHeartbeatMetadata.Add(x.ToArray()));
-
- // Act
- var client = await Fixture.CreateStreamingHubClientAsync(receiver, options);
- await Task.Delay(650);
- await client.DisposeAsync();
-
- // Assert
- Assert.Equal(2, receivedHeartbeatMetadata.Count);
- }
-}
-
-public interface IStreamingHubHeartbeatTestHub : IStreamingHub;
-public interface IStreamingHubHeartbeatTestHub_EnableByAttribute : IStreamingHub;
-public interface IStreamingHubHeartbeatTestHub_DisableByAttribute : IStreamingHub;
-public interface IStreamingHubHeartbeatTestHub_CustomIntervalAndTimeout : IStreamingHub;
-public interface IStreamingHubHeartbeatTestHub_TimeoutBehavior : IStreamingHub;
-public interface IStreamingHubHeartbeatTestHubReceiver;
-
-// Implementations
-
-// This streaming hub has no `Heartbeat` attribute.
-public class StreamingHubHeartbeatTestHub()
- : StreamingHubBase, IStreamingHubHeartbeatTestHub;
-
-[Heartbeat]
-public class StreamingHubHeartbeatTestHub_EnableByAttribute()
- : StreamingHubBase, IStreamingHubHeartbeatTestHub_EnableByAttribute;
-
-[Heartbeat(Enable = false)]
-public class StreamingHubHeartbeatTestHub_DisableByAttribute()
- : StreamingHubBase, IStreamingHubHeartbeatTestHub_DisableByAttribute;
-
-[Heartbeat(Interval = 500, Timeout = 100)]
-public class StreamingHubHeartbeatTestHub_CustomIntervalAndTimeout()
- : StreamingHubBase, IStreamingHubHeartbeatTestHub_CustomIntervalAndTimeout;
-
-[Heartbeat(Enable = true, Interval = 200, Timeout = 100)]
-public class StreamingHubHeartbeatTestHub_TimeoutBehavior([FromKeyedServices(ServerFixture.ItemsServiceKey)] ConcurrentDictionary items)
- : StreamingHubBase, IStreamingHubHeartbeatTestHub_TimeoutBehavior
-{
- protected override ValueTask OnDisconnected()
- {
- items["Disconnected"] = true;
- return base.OnDisconnected();
- }
-}
diff --git a/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubServerHeartbeatTest.cs b/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubServerHeartbeatTest.cs
new file mode 100644
index 000000000..bdbd32f38
--- /dev/null
+++ b/tests/MagicOnion.Server.Tests/StreamingHubHeartbeat/StreamingHubServerHeartbeatTest.cs
@@ -0,0 +1,218 @@
+using System.Collections.Concurrent;
+using MagicOnion.Client;
+using MagicOnion.Server.Hubs;
+using Microsoft.Extensions.DependencyInjection;
+using NSubstitute;
+
+namespace MagicOnion.Server.Tests.StreamingHubHeartbeat;
+
+public abstract class StreamingHubServerHeartbeatTestBase
+{
+ protected ServerFixture Fixture { get; }
+
+ public StreamingHubServerHeartbeatTestBase(ServerFixture fixture)
+ {
+ this.Fixture = fixture;
+ }
+
+ [Fact]
+ public async Task EnableByAttribute()
+ {
+ // Arrange
+ var receiver = Substitute.For();
+ var receivedHeartbeatMetadata = new List();
+ var options = StreamingHubClientOptions.CreateWithDefault().WithServerHeartbeatReceived(x => receivedHeartbeatMetadata.Add(x.ToArray()));
+
+ // Act
+ var client = await Fixture.CreateStreamingHubClientAsync(receiver, options);
+ await Task.Delay(650);
+ await client.DisposeAsync();
+
+ // Assert
+ Assert.Equal(2, receivedHeartbeatMetadata.Count); // The client must receive a heartbeat every 300ms from the server.
+ }
+
+ [Fact]
+ public async Task DisableByAttribute()
+ {
+ // Arrange
+ var receiver = Substitute.For();
+ var receivedHeartbeatMetadata = new List();
+ var options = StreamingHubClientOptions.CreateWithDefault().WithServerHeartbeatReceived(x => receivedHeartbeatMetadata.Add(x.ToArray()));
+
+ // Act
+ var client = await Fixture.CreateStreamingHubClientAsync(receiver, options);
+ await Task.Delay(650);
+ await client.DisposeAsync();
+
+ // Assert
+ Assert.Empty(receivedHeartbeatMetadata);
+ }
+
+ [Fact]
+ public async Task Override_Interval()
+ {
+ // Arrange
+ var receiver = Substitute.For();
+ var receivedHeartbeatMetadata = new List();
+ var options = StreamingHubClientOptions.CreateWithDefault().WithServerHeartbeatReceived(x => receivedHeartbeatMetadata.Add(x.ToArray()));
+
+ // Act
+ var client = await Fixture.CreateStreamingHubClientAsync(receiver, options);
+ await Task.Delay(650);
+ await client.DisposeAsync();
+
+ // Assert
+ Assert.Single(receivedHeartbeatMetadata); // The client must receive a heartbeat every 500ms from the server.
+ }
+
+ [Fact]
+ public async Task Timeout()
+ {
+ // Arrange
+ var heartbeatReceived = new TaskCompletionSource();
+ var receiver = Substitute.For();
+ var options = StreamingHubClientOptions.CreateWithDefault().WithServerHeartbeatReceived(x =>
+ {
+ heartbeatReceived.SetResult();
+ Thread.Sleep(200); // Block consuming message loop.
+ });
+
+ // We need to consume message inline. Avoid post continuations to the synchronization context.
+ SynchronizationContext.SetSynchronizationContext(null);
+
+ // Act
+ var client = await Fixture.CreateStreamingHubClientAsync(receiver, options);
+
+ // Wait for receiving a heartbeat from the server.
+ // The client must receive a heartbeat every 200ms from the server.
+ await heartbeatReceived.Task.WaitAsync(TimeSpan.FromSeconds(1));
+
+ // Timeout at 100 ms after receiving a heartbeat.
+ await Task.Delay(500);
+
+ // Assert
+ Assert.True((bool)Fixture.Items.GetValueOrDefault("Disconnected"));
+ Assert.True(client.WaitForDisconnect().IsCompleted);
+ }
+}
+
+
+public class StreamingHubServerHeartbeatTest_DisabledByDefault : StreamingHubServerHeartbeatTestBase, IClassFixture
+{
+ public StreamingHubServerHeartbeatTest_DisabledByDefault(StreamingHubHeartbeatTestServerFixture fixture)
+ : base(fixture)
+ {
+ }
+
+ public class StreamingHubHeartbeatTestServerFixture : ServerFixture<
+ StreamingHubServerHeartbeatTestHub,
+ StreamingHubServerHeartbeatTestHub_EnableByAttribute,
+ StreamingHubServerHeartbeatTestHub_DisableByAttribute,
+ StreamingHubServerHeartbeatTestHub_CustomIntervalAndTimeout,
+ StreamingHubServerHeartbeatTestHub_TimeoutBehavior
+ >
+ {
+ protected override void ConfigureMagicOnion(MagicOnionOptions options)
+ {
+ options.StreamingHubHeartbeatInterval = TimeSpan.FromMilliseconds(300);
+ options.StreamingHubHeartbeatTimeout = TimeSpan.FromMilliseconds(200);
+ options.EnableStreamingHubHeartbeat = false; // Disabled by default.
+ }
+ }
+
+ [Fact]
+ public async Task Default_Disable()
+ {
+ // Arrange
+ var receiver = Substitute.For();
+ var receivedHeartbeatMetadata = new List();
+ var options = StreamingHubClientOptions.CreateWithDefault().WithServerHeartbeatReceived(x => receivedHeartbeatMetadata.Add(x.ToArray()));
+
+ // Act
+ var client = await Fixture.CreateStreamingHubClientAsync(receiver, options);
+ await Task.Delay(650);
+ await client.DisposeAsync();
+
+ // Assert
+ Assert.Empty(receivedHeartbeatMetadata);
+ }
+}
+
+
+public class StreamingHubServerHeartbeatTest_EnabledByDefault : StreamingHubServerHeartbeatTestBase, IClassFixture
+{
+ public StreamingHubServerHeartbeatTest_EnabledByDefault(StreamingHubHeartbeatTestServerFixture fixture)
+ : base(fixture)
+ {
+ }
+
+ public class StreamingHubHeartbeatTestServerFixture : ServerFixture<
+ StreamingHubServerHeartbeatTestHub,
+ StreamingHubServerHeartbeatTestHub_EnableByAttribute,
+ StreamingHubServerHeartbeatTestHub_DisableByAttribute,
+ StreamingHubServerHeartbeatTestHub_CustomIntervalAndTimeout,
+ StreamingHubServerHeartbeatTestHub_TimeoutBehavior
+ >
+ {
+ protected override void ConfigureMagicOnion(MagicOnionOptions options)
+ {
+ options.StreamingHubHeartbeatInterval = TimeSpan.FromMilliseconds(300);
+ options.StreamingHubHeartbeatTimeout = TimeSpan.FromMilliseconds(200);
+ options.EnableStreamingHubHeartbeat = true; // Enabled by default.
+ }
+ }
+
+ [Fact]
+ public async Task Default_Enable()
+ {
+ // Arrange
+ var receiver = Substitute.For();
+ var receivedHeartbeatMetadata = new List();
+ var options = StreamingHubClientOptions.CreateWithDefault().WithServerHeartbeatReceived(x => receivedHeartbeatMetadata.Add(x.ToArray()));
+
+ // Act
+ var client = await Fixture.CreateStreamingHubClientAsync(receiver, options);
+ await Task.Delay(650);
+ await client.DisposeAsync();
+
+ // Assert
+ Assert.Equal(2, receivedHeartbeatMetadata.Count);
+ }
+}
+
+public interface IStreamingHubServerHeartbeatTestHub : IStreamingHub;
+public interface IStreamingHubServerHeartbeatTestHub_EnableByAttribute : IStreamingHub;
+public interface IStreamingHubServerHeartbeatTestHub_DisableByAttribute : IStreamingHub;
+public interface IStreamingHubServerHeartbeatTestHub_CustomIntervalAndTimeout : IStreamingHub;
+public interface IStreamingHubServerHeartbeatTestHub_TimeoutBehavior : IStreamingHub;
+public interface IStreamingHubServerHeartbeatTestHubReceiver;
+
+// Implementations
+
+// This streaming hub has no `Heartbeat` attribute.
+public class StreamingHubServerHeartbeatTestHub()
+ : StreamingHubBase, IStreamingHubServerHeartbeatTestHub;
+
+[Heartbeat]
+public class StreamingHubServerHeartbeatTestHub_EnableByAttribute()
+ : StreamingHubBase, IStreamingHubServerHeartbeatTestHub_EnableByAttribute;
+
+[Heartbeat(Enable = false)]
+public class StreamingHubServerHeartbeatTestHub_DisableByAttribute()
+ : StreamingHubBase, IStreamingHubServerHeartbeatTestHub_DisableByAttribute;
+
+[Heartbeat(Interval = 500, Timeout = 100)]
+public class StreamingHubServerHeartbeatTestHub_CustomIntervalAndTimeout()
+ : StreamingHubBase, IStreamingHubServerHeartbeatTestHub_CustomIntervalAndTimeout;
+
+[Heartbeat(Enable = true, Interval = 200, Timeout = 100)]
+public class StreamingHubServerHeartbeatTestHub_TimeoutBehavior([FromKeyedServices(ServerFixture.ItemsServiceKey)] ConcurrentDictionary items)
+ : StreamingHubBase, IStreamingHubServerHeartbeatTestHub_TimeoutBehavior
+{
+ protected override ValueTask OnDisconnected()
+ {
+ items["Disconnected"] = true;
+ return base.OnDisconnected();
+ }
+}