mayu-live 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +661 -0
  3. data/README.md +598 -0
  4. data/exe/mayu +33 -0
  5. data/lib/mayu/app_metrics.rb +93 -0
  6. data/lib/mayu/banner.rb +12 -0
  7. data/lib/mayu/client/README.md +17 -0
  8. data/lib/mayu/client/dist/DecompressionStreamPolyfill-3ceba43e.js +1 -0
  9. data/lib/mayu/client/dist/DecompressionStreamPolyfill-3ceba43e.js.br +0 -0
  10. data/lib/mayu/client/dist/DecompressionStreamPolyfill-3ceba43e.js.map +1 -0
  11. data/lib/mayu/client/dist/DecompressionStreamPolyfill-3ceba43e.js.map.br +0 -0
  12. data/lib/mayu/client/dist/custom-elements/mayu-alert-cd7ad2a4.js +1 -0
  13. data/lib/mayu/client/dist/custom-elements/mayu-alert-cd7ad2a4.js.map +1 -0
  14. data/lib/mayu/client/dist/custom-elements/mayu-disconnected-9f349f46.js +1 -0
  15. data/lib/mayu/client/dist/custom-elements/mayu-disconnected-9f349f46.js.map +1 -0
  16. data/lib/mayu/client/dist/custom-elements/mayu-exception-63df4e8c.js +1 -0
  17. data/lib/mayu/client/dist/custom-elements/mayu-exception-63df4e8c.js.map +1 -0
  18. data/lib/mayu/client/dist/custom-elements/mayu-ping-c498c2a6.js +1 -0
  19. data/lib/mayu/client/dist/custom-elements/mayu-ping-c498c2a6.js.map +1 -0
  20. data/lib/mayu/client/dist/custom-elements/mayu-progress-bar-eb3e1ac8.js +1 -0
  21. data/lib/mayu/client/dist/custom-elements/mayu-progress-bar-eb3e1ac8.js.map +1 -0
  22. data/lib/mayu/client/dist/entries.json +3 -0
  23. data/lib/mayu/client/dist/main-4b49dbc4.js +1 -0
  24. data/lib/mayu/client/dist/main-4b49dbc4.js.br +0 -0
  25. data/lib/mayu/client/dist/main-4b49dbc4.js.map +1 -0
  26. data/lib/mayu/client/dist/main-4b49dbc4.js.map.br +0 -0
  27. data/lib/mayu/client/package.json +39 -0
  28. data/lib/mayu/client/rollup.config.js +81 -0
  29. data/lib/mayu/client/src/DecompressionStream.ts +15 -0
  30. data/lib/mayu/client/src/DecompressionStreamPolyfill.ts +43 -0
  31. data/lib/mayu/client/src/MimeTypes.ts +4 -0
  32. data/lib/mayu/client/src/NodeTree.ts +445 -0
  33. data/lib/mayu/client/src/custom-elements/mayu-alert.html +137 -0
  34. data/lib/mayu/client/src/custom-elements/mayu-alert.ts +62 -0
  35. data/lib/mayu/client/src/custom-elements/mayu-disconnected.html +134 -0
  36. data/lib/mayu/client/src/custom-elements/mayu-disconnected.ts +51 -0
  37. data/lib/mayu/client/src/custom-elements/mayu-exception.html +79 -0
  38. data/lib/mayu/client/src/custom-elements/mayu-exception.ts +28 -0
  39. data/lib/mayu/client/src/custom-elements/mayu-log.html +70 -0
  40. data/lib/mayu/client/src/custom-elements/mayu-log.ts +42 -0
  41. data/lib/mayu/client/src/custom-elements/mayu-ping.html +36 -0
  42. data/lib/mayu/client/src/custom-elements/mayu-ping.ts +53 -0
  43. data/lib/mayu/client/src/custom-elements/mayu-progress-bar.html +44 -0
  44. data/lib/mayu/client/src/custom-elements/mayu-progress-bar.ts +40 -0
  45. data/lib/mayu/client/src/custom-elements/types.d.ts +4 -0
  46. data/lib/mayu/client/src/global.d.ts +26 -0
  47. data/lib/mayu/client/src/h.ts +27 -0
  48. data/lib/mayu/client/src/logger.ts +56 -0
  49. data/lib/mayu/client/src/main.ts +271 -0
  50. data/lib/mayu/client/src/serializeEvent.ts +90 -0
  51. data/lib/mayu/client/src/stream.ts +175 -0
  52. data/lib/mayu/client/src/types.ts +1 -0
  53. data/lib/mayu/client/src/utils.ts +71 -0
  54. data/lib/mayu/client/tsconfig.json +18 -0
  55. data/lib/mayu/colors.rb +34 -0
  56. data/lib/mayu/commands/base.rb +22 -0
  57. data/lib/mayu/commands/build.rb +82 -0
  58. data/lib/mayu/commands.rb +53 -0
  59. data/lib/mayu/component/base.rb +177 -0
  60. data/lib/mayu/component/handler_ref.rb +99 -0
  61. data/lib/mayu/component/helpers.rb +93 -0
  62. data/lib/mayu/component/interface.rb +18 -0
  63. data/lib/mayu/component/wrapper.rb +165 -0
  64. data/lib/mayu/component.rb +54 -0
  65. data/lib/mayu/configuration.rb +195 -0
  66. data/lib/mayu/disable_sorbet.rb +23 -0
  67. data/lib/mayu/environment.rb +151 -0
  68. data/lib/mayu/event_stream.rb +158 -0
  69. data/lib/mayu/fetch.rb +88 -0
  70. data/lib/mayu/html.rb +53 -0
  71. data/lib/mayu/html.yaml +767 -0
  72. data/lib/mayu/message_cipher.rb +172 -0
  73. data/lib/mayu/message_cipher.test.rb +16 -0
  74. data/lib/mayu/metrics/collector.rb +161 -0
  75. data/lib/mayu/metrics/exporter.rb +47 -0
  76. data/lib/mayu/metrics/reporter.rb +187 -0
  77. data/lib/mayu/metrics.rb +82 -0
  78. data/lib/mayu/ref_counter.rb +57 -0
  79. data/lib/mayu/resources/README.md +14 -0
  80. data/lib/mayu/resources/asset.rb +71 -0
  81. data/lib/mayu/resources/assets.rb +76 -0
  82. data/lib/mayu/resources/dependency_graph.rb +306 -0
  83. data/lib/mayu/resources/dot_exporter.rb +167 -0
  84. data/lib/mayu/resources/generators/base.rb +18 -0
  85. data/lib/mayu/resources/generators/copy_file.rb +26 -0
  86. data/lib/mayu/resources/generators/image.rb +106 -0
  87. data/lib/mayu/resources/generators/write_file.rb +39 -0
  88. data/lib/mayu/resources/hot_swap/file_watcher.rb +69 -0
  89. data/lib/mayu/resources/hot_swap.rb +46 -0
  90. data/lib/mayu/resources/mermaid_exporter.rb +210 -0
  91. data/lib/mayu/resources/registry.rb +190 -0
  92. data/lib/mayu/resources/resolver/base.rb +32 -0
  93. data/lib/mayu/resources/resolver/filesystem.rb +94 -0
  94. data/lib/mayu/resources/resolver/static.rb +27 -0
  95. data/lib/mayu/resources/resolver.rb +13 -0
  96. data/lib/mayu/resources/resource.rb +150 -0
  97. data/lib/mayu/resources/transformers/__test__/css/adjacent_selectors.in.css +3 -0
  98. data/lib/mayu/resources/transformers/__test__/css/adjacent_selectors.out.css +6 -0
  99. data/lib/mayu/resources/transformers/__test__/css/attributes.in.css +3 -0
  100. data/lib/mayu/resources/transformers/__test__/css/attributes.out.css +6 -0
  101. data/lib/mayu/resources/transformers/__test__/css/composes.in.css +6 -0
  102. data/lib/mayu/resources/transformers/__test__/css/composes.out.css +10 -0
  103. data/lib/mayu/resources/transformers/__test__/css/element_selectors.in.css +3 -0
  104. data/lib/mayu/resources/transformers/__test__/css/element_selectors.out.css +6 -0
  105. data/lib/mayu/resources/transformers/__test__/css/has.in.css +7 -0
  106. data/lib/mayu/resources/transformers/__test__/css/has.out.css +10 -0
  107. data/lib/mayu/resources/transformers/__test__/css/media_queries.in.css +8 -0
  108. data/lib/mayu/resources/transformers/__test__/css/media_queries.out.css +12 -0
  109. data/lib/mayu/resources/transformers/__test__/css/pseudo_classes.in.css +5 -0
  110. data/lib/mayu/resources/transformers/__test__/css/pseudo_classes.out.css +6 -0
  111. data/lib/mayu/resources/transformers/__test__/haml/README.md +10 -0
  112. data/lib/mayu/resources/transformers/__test__/haml/case.haml +8 -0
  113. data/lib/mayu/resources/transformers/__test__/haml/case.rb +15 -0
  114. data/lib/mayu/resources/transformers/__test__/haml/class_names.haml +13 -0
  115. data/lib/mayu/resources/transformers/__test__/haml/class_names.rb +26 -0
  116. data/lib/mayu/resources/transformers/__test__/haml/comments.haml +5 -0
  117. data/lib/mayu/resources/transformers/__test__/haml/comments.rb +5 -0
  118. data/lib/mayu/resources/transformers/__test__/haml/css.haml +3 -0
  119. data/lib/mayu/resources/transformers/__test__/haml/css.rb +11 -0
  120. data/lib/mayu/resources/transformers/__test__/haml/dashes.haml +3 -0
  121. data/lib/mayu/resources/transformers/__test__/haml/dashes.rb +11 -0
  122. data/lib/mayu/resources/transformers/__test__/haml/early_return.haml +4 -0
  123. data/lib/mayu/resources/transformers/__test__/haml/early_return.rb +9 -0
  124. data/lib/mayu/resources/transformers/__test__/haml/early_return2.haml +3 -0
  125. data/lib/mayu/resources/transformers/__test__/haml/early_return2.rb +6 -0
  126. data/lib/mayu/resources/transformers/__test__/haml/handlers.haml +6 -0
  127. data/lib/mayu/resources/transformers/__test__/haml/handlers.rb +12 -0
  128. data/lib/mayu/resources/transformers/__test__/haml/if_else.haml +6 -0
  129. data/lib/mayu/resources/transformers/__test__/haml/if_else.rb +12 -0
  130. data/lib/mayu/resources/transformers/__test__/haml/interpolation.haml +8 -0
  131. data/lib/mayu/resources/transformers/__test__/haml/interpolation.rb +11 -0
  132. data/lib/mayu/resources/transformers/__test__/haml/object_ref_as_key.haml +1 -0
  133. data/lib/mayu/resources/transformers/__test__/haml/object_ref_as_key.rb +5 -0
  134. data/lib/mayu/resources/transformers/__test__/haml/props.haml +4 -0
  135. data/lib/mayu/resources/transformers/__test__/haml/props.rb +11 -0
  136. data/lib/mayu/resources/transformers/__test__/haml/slots.haml +5 -0
  137. data/lib/mayu/resources/transformers/__test__/haml/slots.rb +9 -0
  138. data/lib/mayu/resources/transformers/__test__/haml/slots_dynamic.haml +3 -0
  139. data/lib/mayu/resources/transformers/__test__/haml/slots_dynamic.rb +9 -0
  140. data/lib/mayu/resources/transformers/__test__/haml/slots_fallback.haml +3 -0
  141. data/lib/mayu/resources/transformers/__test__/haml/slots_fallback.rb +5 -0
  142. data/lib/mayu/resources/transformers/__test__/haml/spacing.haml +5 -0
  143. data/lib/mayu/resources/transformers/__test__/haml/spacing.rb +14 -0
  144. data/lib/mayu/resources/transformers/__test__/haml/spacing2.haml +10 -0
  145. data/lib/mayu/resources/transformers/__test__/haml/spacing2.rb +11 -0
  146. data/lib/mayu/resources/transformers/__test__/haml/spacing3.haml +3 -0
  147. data/lib/mayu/resources/transformers/__test__/haml/spacing3.rb +10 -0
  148. data/lib/mayu/resources/transformers/css/rouge_lexer.rb +841 -0
  149. data/lib/mayu/resources/transformers/css.rb +100 -0
  150. data/lib/mayu/resources/transformers/css.test.rb +87 -0
  151. data/lib/mayu/resources/transformers/haml.rb +984 -0
  152. data/lib/mayu/resources/transformers/haml.test.rb +114 -0
  153. data/lib/mayu/resources/types/README.md +36 -0
  154. data/lib/mayu/resources/types/base.rb +35 -0
  155. data/lib/mayu/resources/types/component.rb +198 -0
  156. data/lib/mayu/resources/types/image.rb +169 -0
  157. data/lib/mayu/resources/types/javascript.rb +50 -0
  158. data/lib/mayu/resources/types/nil.rb +23 -0
  159. data/lib/mayu/resources/types/stylesheet.rb +119 -0
  160. data/lib/mayu/resources/types/svg.rb +69 -0
  161. data/lib/mayu/resources/types.rb +37 -0
  162. data/lib/mayu/routes.rb +170 -0
  163. data/lib/mayu/routing/builder.rb +108 -0
  164. data/lib/mayu/routing/matcher.rb +58 -0
  165. data/lib/mayu/routing/routes.rb +85 -0
  166. data/lib/mayu/routing.rb +17 -0
  167. data/lib/mayu/server/app.rb +494 -0
  168. data/lib/mayu/server/controller.rb +152 -0
  169. data/lib/mayu/server/errors.rb +110 -0
  170. data/lib/mayu/server/file_server.rb +140 -0
  171. data/lib/mayu/server.rb +63 -0
  172. data/lib/mayu/session.rb +358 -0
  173. data/lib/mayu/state/README.md +6 -0
  174. data/lib/mayu/state/action_creator.rb +191 -0
  175. data/lib/mayu/state/action_wrapper.rb +30 -0
  176. data/lib/mayu/state/loader.rb +220 -0
  177. data/lib/mayu/state/store.rb +82 -0
  178. data/lib/mayu/state.rb +8 -0
  179. data/lib/mayu/state.test.rb +97 -0
  180. data/lib/mayu/utils.rb +114 -0
  181. data/lib/mayu/vdom/children.rb +117 -0
  182. data/lib/mayu/vdom/component_marshaler.rb +53 -0
  183. data/lib/mayu/vdom/css_attributes.rb +131 -0
  184. data/lib/mayu/vdom/descriptor.rb +151 -0
  185. data/lib/mayu/vdom/descriptor.test.rb +26 -0
  186. data/lib/mayu/vdom/dom.rb +239 -0
  187. data/lib/mayu/vdom/h.rb +22 -0
  188. data/lib/mayu/vdom/id_generator.rb +55 -0
  189. data/lib/mayu/vdom/interfaces.rb +186 -0
  190. data/lib/mayu/vdom/marshalling.rb +78 -0
  191. data/lib/mayu/vdom/reconciliation.rb +205 -0
  192. data/lib/mayu/vdom/reconciliation.test.rb +56 -0
  193. data/lib/mayu/vdom/special_elements.rb +108 -0
  194. data/lib/mayu/vdom/update_context.rb +180 -0
  195. data/lib/mayu/vdom/vdom.perf.test.rb +146 -0
  196. data/lib/mayu/vdom/vnode.rb +266 -0
  197. data/lib/mayu/vdom/vtree.rb +672 -0
  198. data/lib/mayu/vdom/vtree.test.rb +68 -0
  199. data/lib/mayu/vdom.rb +8 -0
  200. data/lib/mayu/vdom.test.rb +73 -0
  201. data/lib/mayu/version.rb +6 -0
  202. data/lib/mayu.rb +8 -0
  203. data/mayu-live.gemspec +70 -0
  204. metadata +612 -0
@@ -0,0 +1,110 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Mayu
5
+ module Server
6
+ module Errors
7
+ extend T::Sig
8
+
9
+ class ServerError < StandardError
10
+ end
11
+
12
+ class FileNotFound < ServerError
13
+ end
14
+ class CookieNotSet < ServerError
15
+ end
16
+ class InvalidToken < ServerError
17
+ end
18
+ class InvalidMethod < ServerError
19
+ end
20
+ class UnauthorizedSessionCookie < ServerError
21
+ end
22
+ class SessionAlreadyResumed < ServerError
23
+ end
24
+ class SessionNotFound < ServerError
25
+ end
26
+ class ServerIsShuttingDown < ServerError
27
+ end
28
+ class InvalidSecFetchHeader < ServerError
29
+ end
30
+
31
+ sig do
32
+ params(block: T.proc.returns(Protocol::HTTP::Response)).returns(
33
+ Protocol::HTTP::Response
34
+ )
35
+ end
36
+ def self.handle_exceptions(&block)
37
+ respond_to_exceptions { log_exceptions { yield } }
38
+ end
39
+
40
+ sig do
41
+ params(block: T.proc.returns(Protocol::HTTP::Response)).returns(
42
+ Protocol::HTTP::Response
43
+ )
44
+ end
45
+ def self.log_exceptions(&block)
46
+ yield
47
+ rescue => e
48
+ Console.logger.error(self, "#{e.class.name}: #{e.message}")
49
+ raise
50
+ end
51
+
52
+ sig do
53
+ params(block: T.proc.returns(Protocol::HTTP::Response)).returns(
54
+ Protocol::HTTP::Response
55
+ )
56
+ end
57
+ def self.respond_to_exceptions(&block)
58
+ yield
59
+ rescue Errno::ENOENT => e
60
+ text_response(404, "file not found")
61
+ rescue FileNotFound => e
62
+ text_response(404, e.message.to_s)
63
+ rescue CookieNotSet => e
64
+ text_response(403, "session cookie not set")
65
+ rescue SessionNotFound => e
66
+ text_response(404, "session not found")
67
+ rescue Mayu::MessageCipher::DecryptError => e
68
+ text_response(403, "decrypt error")
69
+ rescue Mayu::MessageCipher::ExpiredError => e
70
+ text_response(403, "session expired")
71
+ rescue InvalidToken => e
72
+ text_response(403, "invalid token")
73
+ rescue SessionAlreadyResumed => e
74
+ text_response(409, "already resumed")
75
+ rescue Session::AlreadyRunningError => e
76
+ text_response(409, "already running")
77
+ rescue ServerIsShuttingDown => e
78
+ # https://fly.io/docs/reference/fly-replay/#fly-replay
79
+ text_response(
80
+ 405,
81
+ "invalid method",
82
+ { "fly-replay" => "elsewhere=true" }
83
+ )
84
+ rescue InvalidMethod => e
85
+ text_response(405, "invalid method")
86
+ rescue UnauthorizedSessionCookie => e
87
+ text_response(403, "session cookie is invalid")
88
+ rescue InvalidSecFetchHeader => e
89
+ text_response(415, e.message)
90
+ rescue StandardError
91
+ text_response(500, "error")
92
+ end
93
+
94
+ sig do
95
+ params(
96
+ code: Integer,
97
+ text: String,
98
+ headers: T::Hash[String, String]
99
+ ).returns(Protocol::HTTP::Response)
100
+ end
101
+ def self.text_response(code, text, headers = {})
102
+ Protocol::HTTP::Response[
103
+ code,
104
+ { "content-type" => "text/plain", **headers },
105
+ [text]
106
+ ]
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,140 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "errors"
5
+
6
+ module Mayu
7
+ module Server
8
+ class FileServer
9
+ class FoundFile < T::Struct
10
+ const :absolute_path, String
11
+ const :content_type, String
12
+ const :size, Integer
13
+ end
14
+
15
+ # TODO: Make configurable. A higher value means less
16
+ # filsystem IO, but obviously consumes more memory.
17
+ DEFAULT_MEMORY_CACHE_MAX_SIZE = T.let(1024, Integer)
18
+
19
+ CACHE_MAX_AGE = T.let(60 * 60 * 24 * 7, Integer)
20
+
21
+ CACHE_CONTROL =
22
+ T.let(
23
+ {
24
+ "cache-control" => "public, max-age=#{CACHE_MAX_AGE}, immutable"
25
+ }.freeze,
26
+ T::Hash[String, String]
27
+ )
28
+ BROTLI_CONTENT_ENCODING =
29
+ T.let({ "content-encoding" => "br" }.freeze, T::Hash[String, String])
30
+
31
+ extend T::Sig
32
+
33
+ sig { params(root_dir: String, memory_cache_max_size: Integer).void }
34
+ def initialize(
35
+ root_dir,
36
+ memory_cache_max_size: DEFAULT_MEMORY_CACHE_MAX_SIZE
37
+ )
38
+ @root_dir = root_dir
39
+ @found_files =
40
+ T.let(
41
+ T::Hash[String, FoundFile].new do |h, filename|
42
+ if found_file = find_file(filename)
43
+ h[filename] = found_file
44
+ end
45
+ end,
46
+ T::Hash[String, FoundFile]
47
+ )
48
+ @memory_cache_max_size = memory_cache_max_size
49
+ @memory_cache = T.let({}, T::Hash[String, String])
50
+ end
51
+
52
+ sig do
53
+ params(filename: String, accept_encodings: T::Array[String]).returns(
54
+ Protocol::HTTP::Response
55
+ )
56
+ end
57
+ def serve(filename, accept_encodings: [])
58
+ found_file = @found_files[filename]
59
+
60
+ unless found_file
61
+ raise Errors::FileNotFound, "Could not find file #{filename}"
62
+ end
63
+
64
+ headers = {
65
+ **CACHE_CONTROL,
66
+ "content-type" => add_charset(found_file.content_type)
67
+ }
68
+
69
+ if accept_encodings.include?("br")
70
+ if brotlied = @found_files["#{filename}.br"]
71
+ return(
72
+ Protocol::HTTP::Response[
73
+ 200,
74
+ { **headers, **BROTLI_CONTENT_ENCODING },
75
+ read_file(brotlied)
76
+ ]
77
+ )
78
+ end
79
+ end
80
+
81
+ contents = read_file(found_file)
82
+ Protocol::HTTP::Response[200, headers, read_file(found_file)]
83
+ end
84
+
85
+ private
86
+
87
+ sig { params(filename: String).returns(T.nilable(FoundFile)) }
88
+ def find_file(filename)
89
+ absolute_path = File.join(@root_dir, filename)
90
+
91
+ return unless File.exist?(absolute_path)
92
+
93
+ size = File.size(absolute_path)
94
+
95
+ content_type =
96
+ MIME::Types.type_for(absolute_path.delete_suffix(".br")).first.to_s
97
+
98
+ FoundFile.new(absolute_path:, content_type:, size:)
99
+ end
100
+
101
+ sig do
102
+ params(found_file: FoundFile).returns(
103
+ T.any([String], Protocol::HTTP::Body::File)
104
+ )
105
+ end
106
+ def read_file(found_file)
107
+ if found_file.size > @memory_cache_max_size
108
+ return Protocol::HTTP::Body::File.open(found_file.absolute_path)
109
+ end
110
+
111
+ [
112
+ @memory_cache[found_file.absolute_path] ||= begin
113
+ File.read(found_file.absolute_path)
114
+ end
115
+ ]
116
+ end
117
+
118
+ sig { params(filename: String).returns(String) }
119
+ def get_absolute_path(filename)
120
+ File.join(@root_dir, File.expand_path(filename, "/"))
121
+ end
122
+
123
+ sig { params(content_type: String).returns(String) }
124
+ def add_charset(content_type)
125
+ if add_charset?(content_type)
126
+ "#{content_type}; charset=utf-8"
127
+ else
128
+ content_type
129
+ end
130
+ end
131
+
132
+ sig { params(content_type: String).returns(T::Boolean) }
133
+ def add_charset?(content_type)
134
+ content_type in
135
+ "application/javascript" | "application/json" | "image/svg+xml" |
136
+ "text/css" | "text/html"
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,63 @@
1
+ # typed: strict
2
+
3
+ require "bundler/setup"
4
+ require "sorbet-runtime"
5
+ require "async"
6
+ require "async/container"
7
+ require "async/http/server"
8
+ require "async/http/endpoint"
9
+ require "protocol/http/body/file"
10
+ require "async/io/host_endpoint"
11
+ require "async/io/shared_endpoint"
12
+ require "async/io/ssl_endpoint"
13
+ require "async/io/trap"
14
+ require "localhost"
15
+ require "mime/types"
16
+ require_relative "environment"
17
+ require_relative "session"
18
+ require_relative "configuration"
19
+ require_relative "colors"
20
+ require_relative "server/controller"
21
+
22
+ module Mayu
23
+ module Server
24
+ extend T::Sig
25
+
26
+ sig { params(config: Configuration).void }
27
+ def self.start_dev(config) = start(config)
28
+
29
+ sig { params(config: Configuration).void }
30
+ def self.start_prod(config) = start(config)
31
+
32
+ sig { params(config: Configuration).void }
33
+ def self.start(config)
34
+ uri = config.server.uri
35
+ ssl_context = setup_self_signed_cert(config)
36
+ endpoint = Async::HTTP::Endpoint.new(uri, ssl_context:, reuse_port: true)
37
+
38
+ Configuration.log_config(config)
39
+ Process.setproctitle("mayu #{config.mode} file://#{config.root} #{uri}")
40
+
41
+ # Metrics.setup(config) if config.metrics.enabled
42
+
43
+ Controller.new(config:, endpoint:).run
44
+ end
45
+
46
+ sig do
47
+ params(config: Configuration).returns(T.nilable(OpenSSL::SSL::SSLContext))
48
+ end
49
+ def self.setup_self_signed_cert(config)
50
+ return unless config.server.self_signed_cert
51
+
52
+ authority = Localhost::Authority.fetch(config.server.host)
53
+
54
+ authority.server_context.tap do |context|
55
+ context.alpn_select_cb = lambda { |_| "h2" }
56
+ lambda { |protocols| protocols.include?("h2") ? "h2" : nil }
57
+
58
+ context.alpn_protocols = ["h2"]
59
+ context.session_id_context = "mayu"
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,358 @@
1
+ # typed: strict
2
+
3
+ require "time"
4
+ require "nanoid"
5
+ require_relative "environment"
6
+ require_relative "vdom/vtree"
7
+ require_relative "vdom/marshalling"
8
+ require_relative "event_stream"
9
+
10
+ module Mayu
11
+ class Session
12
+ extend T::Sig
13
+
14
+ class InvalidTokenError < StandardError
15
+ end
16
+ class InvalidIdError < StandardError
17
+ end
18
+ class AlreadyRunningError < StandardError
19
+ end
20
+
21
+ sig do
22
+ params(environment: Environment, path: String).returns(T.attached_class)
23
+ end
24
+ def self.init(environment:, path:)
25
+ new(environment:, path:)
26
+ end
27
+
28
+ sig do
29
+ params(environment: Environment, dumped: String).returns(T.attached_class)
30
+ end
31
+ def self.restore(environment:, dumped:)
32
+ Marshal.restore(
33
+ dumped,
34
+ ->(obj) do
35
+ case obj
36
+ when self
37
+ obj.instance_variable_set(:@environment, environment)
38
+ obj
39
+ when SerializedSession
40
+ obj.to_session(environment)
41
+ else
42
+ obj
43
+ end
44
+ end
45
+ )
46
+ end
47
+
48
+ ID_FORMAT = /\A[A-Za-z0-9_-]{21}\z/
49
+
50
+ TOKEN_LENGTH = 64
51
+ TOKEN_FORMAT = /\A\w{#{TOKEN_LENGTH}}\z/
52
+
53
+ sig { returns(String) }
54
+ def self.generate_token
55
+ SecureRandom.alphanumeric(TOKEN_LENGTH)
56
+ end
57
+
58
+ sig { params(token: String).returns(T::Boolean) }
59
+ def self.valid_token?(token)
60
+ token.match?(TOKEN_FORMAT)
61
+ end
62
+
63
+ sig { params(token: String).void }
64
+ def self.validate_token!(token)
65
+ raise InvalidTokenError unless valid_token?(token)
66
+ end
67
+
68
+ sig { params(id: String).returns(T::Boolean) }
69
+ def self.valid_id?(id)
70
+ id.match?(ID_FORMAT)
71
+ end
72
+
73
+ sig { params(id: String).void }
74
+ def self.validate_id!(id)
75
+ raise InvalidIdError unless valid_id?(id)
76
+ end
77
+
78
+ sig { params(token: String).returns(T::Boolean) }
79
+ def authorized?(token)
80
+ if self.token.length == token.length
81
+ OpenSSL.fixed_length_secure_compare(self.token, token)
82
+ else
83
+ false
84
+ end
85
+ end
86
+
87
+ Marshaled = T.type_alias { [String, String, String, String, String] }
88
+
89
+ sig { returns(String) }
90
+ attr_reader :id
91
+ sig { returns(String) }
92
+ attr_reader :token
93
+ sig { returns(String) }
94
+ attr_reader :path
95
+ sig { returns(T::Hash[String, String]) }
96
+ attr_reader :headers
97
+ sig { returns(Environment) }
98
+ attr_reader :environment
99
+ sig { returns(Float) }
100
+ attr_reader :last_ping_at
101
+ sig { returns(EventStream::Log) }
102
+ attr_reader :log
103
+
104
+ sig { params(timeout_seconds: T.any(Float, Integer)).returns(T::Boolean) }
105
+ def expired?(timeout_seconds = 30)
106
+ seconds_since_last_ping > timeout_seconds
107
+ end
108
+
109
+ sig { returns(Float) }
110
+ def seconds_since_last_ping
111
+ Time.now.to_f - last_ping_at
112
+ end
113
+
114
+ sig do
115
+ params(
116
+ environment: Environment,
117
+ path: String,
118
+ headers: T::Hash[String, String],
119
+ vtree: T.nilable(VDOM::VTree),
120
+ store: T.nilable(State::Store)
121
+ ).void
122
+ end
123
+ def initialize(environment:, path:, headers: {}, vtree: nil, store: nil)
124
+ @environment = environment
125
+ @id = T.let(Nanoid.generate, String)
126
+ @token = T.let(self.class.generate_token, String)
127
+ @path = path
128
+ @headers = headers
129
+ @vtree = T.let(vtree || VDOM::VTree.new(session: self), VDOM::VTree)
130
+ @log = T.let(EventStream::Log.new, EventStream::Log)
131
+ @store =
132
+ T.let(
133
+ store || environment.create_store(initial_state: {}),
134
+ State::Store
135
+ )
136
+ @app = T.let(environment.load_root(path, headers:), VDOM::Descriptor)
137
+ @last_ping_at = T.let(Time.now.to_f, Float)
138
+ @barrier = T.let(Async::Barrier.new, Async::Barrier)
139
+ end
140
+
141
+ sig { void }
142
+ def stop!
143
+ @barrier.stop
144
+ end
145
+
146
+ Writable =
147
+ T.type_alias { T.any(Async::HTTP::Body::Writable, Brotli::Writer) }
148
+
149
+ sig do
150
+ params(body: Writable, task: Async::Task).returns(
151
+ { stylesheets: T::Array[String] }
152
+ )
153
+ end
154
+ def initial_render(body, task: Async::Task.current)
155
+ @vtree.render(@app, lifecycles: false)
156
+
157
+ root = @vtree.root or raise "There is no root"
158
+
159
+ html = root.to_html
160
+ stylesheets =
161
+ @vtree
162
+ .assets
163
+ .select { _1.end_with?(".css") }
164
+ .map { "/__mayu/static/#{_1}" }
165
+
166
+ # freeze
167
+
168
+ # encrypted_session =
169
+ # @environment.message_cipher.dump(SerializedSession.dump_session(self))
170
+
171
+ links = [
172
+ %{<script async type="module" src="/__mayu/runtime/#{environment.init_js}##{id}" crossorigin="same-origin" fetchpriority="high"></script>},
173
+ *stylesheets.map do |stylesheet|
174
+ %{<link rel="stylesheet" href="#{stylesheet}">}
175
+ end
176
+ ].join
177
+
178
+ # scripts = %{<template id="mayu-init">#{encrypted_session}</template>}
179
+ scripts = ""
180
+ body.write("<!DOCTYPE html>\n")
181
+
182
+ task.async do
183
+ @vtree.root&.write_html(body, links:, scripts:)
184
+ body.close
185
+ rescue => e
186
+ p e
187
+ end
188
+
189
+ { stylesheets: }
190
+ end
191
+
192
+ class SerializedSession
193
+ extend T::Sig
194
+
195
+ sig { returns(T::Array[T.untyped]) }
196
+ attr_reader :data
197
+
198
+ sig { params(data: T::Array[T.untyped]).void }
199
+ def initialize(data)
200
+ @data = data
201
+ end
202
+
203
+ sig { params(session: Session).returns(String) }
204
+ def self.dump_session(session)
205
+ Marshal.dump(self.new(session.marshal_dump))
206
+ end
207
+
208
+ sig { params(environment: Environment).returns(Session) }
209
+ def to_session(environment)
210
+ session = Session.allocate
211
+ session.instance_variable_set(:@environment, environment)
212
+ session.marshal_load(@data)
213
+ session
214
+ end
215
+
216
+ sig { returns(T::Array[T.untyped]) }
217
+ def marshal_dump
218
+ @data
219
+ end
220
+
221
+ sig { params(a: T::Array[T.untyped]).void }
222
+ def marshal_load(a)
223
+ @data = a
224
+ end
225
+ end
226
+
227
+ sig { returns(T::Array[T.untyped]) }
228
+ def marshal_dump
229
+ [
230
+ @id,
231
+ @token,
232
+ @path,
233
+ @headers,
234
+ VDOM::Marshalling.dump(@vtree),
235
+ Marshal.dump(@store.state)
236
+ ]
237
+ end
238
+
239
+ sig { params(a: T::Array[T.untyped]).void }
240
+ def marshal_load(a)
241
+ @id, @token, @path, @headers, dumped_vtree, state = a
242
+ @last_ping_at = Time.now.to_f
243
+ @vtree = VDOM::Marshalling.restore(dumped_vtree, session: self)
244
+ @store = @environment.create_store(initial_state: Marshal.restore(state))
245
+ @app = @environment.load_root(@path, headers:)
246
+ @barrier = Async::Barrier.new
247
+ @log = EventStream::Log.new
248
+ end
249
+
250
+ sig do
251
+ params(
252
+ url: String,
253
+ method: Symbol,
254
+ headers: T::Hash[String, String],
255
+ body: T.nilable(String)
256
+ ).returns(Fetch::Response)
257
+ end
258
+ def fetch(url, method: :GET, headers: {}, body: nil)
259
+ @environment.fetch.fetch(url, method:, headers:, body:)
260
+ end
261
+
262
+ sig { void }
263
+ def activity!
264
+ @last_ping_at = Time.now.to_f
265
+ end
266
+
267
+ sig do
268
+ params(callback_id: String, payload: T::Hash[Symbol, T.untyped]).void
269
+ end
270
+ def handle_callback(callback_id, payload = {})
271
+ activity!
272
+ @vtree.handle_callback(callback_id, payload)
273
+ end
274
+
275
+ sig { void }
276
+ def rerender
277
+ @app = @environment.load_root(path, headers:)
278
+ @vtree.replace_root(@app)
279
+ end
280
+
281
+ sig { params(path: String).void }
282
+ def navigate(path)
283
+ Console.logger.info(self, "navigate: #{path.inspect}")
284
+ @app = @environment.load_root(path, headers:)
285
+ @path = path
286
+ @vtree.replace_root(@app)
287
+ end
288
+
289
+ sig do
290
+ params(
291
+ task: Async::Task,
292
+ block: T.proc.params(msg: [Symbol, T.untyped]).void
293
+ ).returns(Async::Barrier)
294
+ end
295
+ def run(task: Async::Task.current, &block)
296
+ root = @vtree.root
297
+
298
+ raise "No root!" unless root
299
+
300
+ barrier = Async::Barrier.new(parent: @barrier)
301
+
302
+ barrier.async do |subtask|
303
+ yield [:init, { ids: root.id_tree }]
304
+
305
+ root.traverse do |vnode|
306
+ if c = vnode.component
307
+ # TODO: Make sure the component isn't already mounted..
308
+ # maybe can check that in the component wrapper?
309
+ # Also, shouldn't this be done once when resuming rather
310
+ # than when starting the event stream?
311
+ c.mount
312
+ end
313
+ end
314
+
315
+ @vtree.render(@app, lifecycles: true)
316
+
317
+ updater = VDOM::VTree::Updater.new(@vtree)
318
+
319
+ updater
320
+ .run(metrics: environment.metrics, task: subtask) do |msg|
321
+ case msg
322
+ in [:patch, patches]
323
+ yield [:patch, patches]
324
+ in [:exception, error]
325
+ yield [:exception, error]
326
+ in [:pong, timestamp]
327
+ yield [:pong, timestamp]
328
+ in [:navigate, href]
329
+ navigate(href)
330
+ yield [:navigate, path: href.force_encoding("utf-8")]
331
+ in [:action, payload]
332
+ yield [:action, payload]
333
+ in [:update_finished, *]
334
+ # noop
335
+ else
336
+ Console.logger.error(self, "Unknown event: #{msg.inspect}")
337
+ end
338
+ end
339
+ .wait
340
+
341
+ barrier.stop
342
+ end
343
+
344
+ barrier.async do
345
+ loop do
346
+ # puts "keep alive task"
347
+ sleep 1
348
+ yield [:keep_alive, nil]
349
+ end
350
+ ensure
351
+ # puts "Stopped this task"
352
+ barrier.stop
353
+ end
354
+
355
+ barrier
356
+ end
357
+ end
358
+ end
@@ -0,0 +1,6 @@
1
+ # `state/`
2
+
3
+ This directory contains classes related to dealing with state.
4
+
5
+ It's inspired by [Redux Toolkit](https://redux-toolkit.js.org/),
6
+ but simplified.