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,494 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "async/variable"
5
+ require_relative "../event_stream"
6
+ require_relative "file_server"
7
+ require_relative "errors"
8
+
9
+ module Mayu
10
+ module Server
11
+ class App
12
+ extend T::Sig
13
+
14
+ DEV_ASSETS_TIMEOUT_SECONDS = 4
15
+ DEV_ASSETS_RETRY_AFTER_SECONDS = 2
16
+ PING_INTERVAL = 2 # seconds
17
+ NANOID_RE = /[\w-]{21}/
18
+
19
+ MIME_TYPES =
20
+ T.let(
21
+ {
22
+ eventstream: "application/vnd.mayu.eventstream",
23
+ session: "application/vnd.mayu.session"
24
+ },
25
+ T::Hash[Symbol, String]
26
+ )
27
+
28
+ sig { params(environment: Environment).void }
29
+ def initialize(environment:)
30
+ @environment = environment
31
+ @metrics = T.let(environment.metrics, AppMetrics)
32
+ @barrier = T.let(Async::Barrier.new, Async::Barrier)
33
+ @stop = T.let(Async::Variable.new, Async::Variable)
34
+ @sessions = T.let({}, T::Hash[String, Session])
35
+
36
+ @runtime_assets =
37
+ T.let(FileServer.new(@environment.js_runtime_path), FileServer)
38
+ @static_assets =
39
+ T.let(FileServer.new(@environment.path(:assets)), FileServer)
40
+ end
41
+
42
+ sig { void }
43
+ def clear_expired_sessions!
44
+ old_size = @sessions.size
45
+
46
+ @sessions.delete_if do |id, session|
47
+ next unless session.expired?(20)
48
+
49
+ Console.logger.warn(self, "Session #{session.id} timed out")
50
+ session.stop!
51
+ @metrics.session_timeout_count.increment
52
+ true
53
+ end
54
+
55
+ unless @sessions.size == old_size
56
+ Console.logger.warn(self, "Session count: #{@sessions.size}")
57
+ end
58
+
59
+ @metrics.session_count.set(@sessions.size)
60
+ end
61
+
62
+ sig { void }
63
+ def stop
64
+ @stop.resolve(true)
65
+ @barrier.wait
66
+ Console.logger.info(self, "Stopped sessions")
67
+ end
68
+
69
+ sig { void }
70
+ def close
71
+ @barrier.wait
72
+ end
73
+
74
+ sig { returns(Integer) }
75
+ def time_ping_value
76
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond).to_i &
77
+ 0x0fffffff
78
+ end
79
+
80
+ sig do
81
+ params(request: Protocol::HTTP::Request).returns(
82
+ Protocol::HTTP::Response
83
+ )
84
+ end
85
+ def call(request)
86
+ # The following line generates very noisy logs,
87
+ # but can be useful when debugging.
88
+ # Console.logger.info(self, "#{request.method} #{request.path}")
89
+
90
+ Errors.handle_exceptions { handle_request(request) }
91
+ end
92
+
93
+ sig { void }
94
+ def rerender
95
+ @sessions.values.each(&:rerender)
96
+ end
97
+
98
+ sig do
99
+ params(
100
+ id: String,
101
+ request: Protocol::HTTP::Request,
102
+ resume: T::Boolean
103
+ ).returns(Session)
104
+ end
105
+ def get_session(id, request, resume: false)
106
+ session = load_session(id, resume ? request.read.to_s : "")
107
+ cookie_value = get_token_cookie_value(request)
108
+
109
+ if session.authorized?(cookie_value)
110
+ session
111
+ else
112
+ raise Errors::UnauthorizedSessionCookie,
113
+ "session with id #{id} had wrong value #{cookie_value.inspect}"
114
+ end
115
+ end
116
+
117
+ sig do
118
+ params(request: Protocol::HTTP::Request).returns(
119
+ Protocol::HTTP::Response
120
+ )
121
+ end
122
+ def handle_request(request)
123
+ # FIXME: raise_if_shutting_down! should only prevent the following:
124
+ # * starting new sessions
125
+ # * updating sessions that have been transferred
126
+ # * updating sessions that have been paused for transferring
127
+ raise_if_shutting_down!
128
+
129
+ case request.path.delete_prefix("/").split("/")
130
+ in ["__mayu", "session", NANOID_RE => session_id, *rest]
131
+ handle_session_post(request, session_id, rest)
132
+ in ["index.js"]
133
+ body = File.read(File.join(__dir__, "client", "dist", "live.js"))
134
+ Protocol::HTTP::Response[
135
+ 200,
136
+ { "content-type": "application/javascript" },
137
+ [body]
138
+ ]
139
+ in ["robots.txt"]
140
+ Protocol::HTTP::Response[
141
+ 200,
142
+ { "content-type" => "text/plain; charset=utf-8" },
143
+ File.read(File.join(@environment.root, "app", "robots.txt"))
144
+ ]
145
+ in ["favicon.ico"]
146
+ # Idea: Maybe it would be possible to create
147
+ # an asset from the favicon and redirect to the asset?
148
+ Protocol::HTTP::Response[
149
+ 200,
150
+ { "content-type" => "image/png" },
151
+ Protocol::HTTP::Body::File.open(
152
+ File.join(@environment.root, "app", "favicon.png")
153
+ )
154
+ ]
155
+ in ["__mayu", "status"]
156
+ Protocol::HTTP::Response[200, {}, "ok"]
157
+ in ["__mayu", "runtime", *path]
158
+ accept_encodings = request.headers["accept-encoding"].to_s.split(", ")
159
+
160
+ filename = File.join(*path)
161
+
162
+ if filename == "entries.json"
163
+ return Protocol::HTTP::Response[403, {}, ["forbidden"]]
164
+ end
165
+
166
+ @runtime_assets.serve(filename, accept_encodings:)
167
+ in ["__mayu", "static", filename]
168
+ if @environment.config.server.generate_assets
169
+ begin
170
+ @environment.resources.wait_for_asset(
171
+ filename,
172
+ timeout: DEV_ASSETS_TIMEOUT_SECONDS
173
+ )
174
+ rescue Async::TimeoutError => e
175
+ Console.logger.warn(
176
+ self,
177
+ "Asset #{filename} could not be generated in time"
178
+ )
179
+ return(
180
+ Protocol::HTTP::Response[
181
+ 503,
182
+ { "retry-after" => DEV_ASSETS_RETRY_AFTER_SECONDS },
183
+ ["asset could not be generated in time"]
184
+ ]
185
+ )
186
+ end
187
+ end
188
+
189
+ accept_encodings = request.headers["accept-encoding"].to_s.split(", ")
190
+
191
+ @static_assets.serve(filename, accept_encodings:)
192
+ in ["__mayu", *]
193
+ raise Errors::FileNotFound,
194
+ "Resource not found at: #{request.method} #{request.path}"
195
+ in [*] if request.method == "GET"
196
+ raise_if_shutting_down!
197
+
198
+ handle_session_init(request)
199
+ else
200
+ Protocol::HTTP::Response[404, {}, ["not found"]]
201
+ end
202
+ end
203
+
204
+ sig { void }
205
+ def raise_if_shutting_down!
206
+ raise Errors::ServerIsShuttingDown if @stop.resolved?
207
+ end
208
+
209
+ sig do
210
+ params(
211
+ request: Protocol::HTTP::Request,
212
+ session_id: String,
213
+ path: T::Array[String]
214
+ ).returns(Protocol::HTTP::Response)
215
+ end
216
+ def handle_session_post(request, session_id, path)
217
+ raise Errors::InvalidMethod unless request.method == "POST"
218
+
219
+ if ["resume"] === path
220
+ body = Async::HTTP::Body::Writable.new
221
+ session = get_session(session_id, request, resume: true)
222
+ run_event_stream(session, body:)
223
+
224
+ return(
225
+ Protocol::HTTP::Response[
226
+ 200,
227
+ { "content-type": MIME_TYPES[:eventstream] },
228
+ body
229
+ ]
230
+ )
231
+ end
232
+
233
+ session = get_session(session_id, request, resume: false)
234
+ session.activity!
235
+
236
+ case path
237
+ in ["init"]
238
+ body = Async::HTTP::Body::Writable.new
239
+ run_event_stream(session, body:)
240
+ Protocol::HTTP::Response[
241
+ 200,
242
+ { "content-type": MIME_TYPES[:eventstream] },
243
+ body
244
+ ]
245
+ in ["ping"]
246
+ body = JSON.parse(request.read.to_s)
247
+ pong = body["pong"].to_f
248
+ ping = body["ping"]
249
+ time = time_ping_value
250
+ server_pong = time_ping_value - body["pong"].to_f
251
+ # Console.logger.info(
252
+ # self,
253
+ # format("Session #{session.id} ping: %.2f ms", server_pong)
254
+ # )
255
+ headers = {
256
+ "content-type": "application/json",
257
+ "set-cookie": set_token_cookie_value(session)
258
+ }
259
+ session.log.push(
260
+ :pong,
261
+ pong: ping,
262
+ server: server_pong,
263
+ region: @environment.config.instance.region,
264
+ instance: @environment.config.instance.alloc_id.split("-", 2).first
265
+ )
266
+ Protocol::HTTP::Response[200, headers, [JSON.generate(ping)]]
267
+ in ["navigate"]
268
+ @environment.metrics.session_navigate_count.increment()
269
+ path = request.read.force_encoding("utf-8")
270
+ session.handle_callback("navigate", { path: })
271
+ Protocol::HTTP::Response[200, headers, ["ok"]]
272
+ in ["callback", String => callback_id]
273
+ session.handle_callback(
274
+ callback_id,
275
+ JSON.parse(request.read, symbolize_names: true)
276
+ )
277
+ headers = { "set-cookie": set_token_cookie_value(session) }
278
+ Protocol::HTTP::Response[200, headers, ["ok"]]
279
+ end
280
+ end
281
+
282
+ sig do
283
+ params(request: Protocol::HTTP::Request).returns(
284
+ Protocol::HTTP::Response
285
+ )
286
+ end
287
+ def handle_session_init(request)
288
+ Console.logger.info(self) { "Init session: #{request.path}" }
289
+
290
+ validate_header!(
291
+ request.headers,
292
+ "sec-fetch-mode",
293
+ "navigate"
294
+ ) do |value|
295
+ raise Errors::InvalidSecFetchHeader,
296
+ "Expected sec-fetch-mode to equal navigate but got #{value.inspect}"
297
+ end
298
+
299
+ validate_header!(
300
+ request.headers,
301
+ "sec-fetch-dest",
302
+ "document"
303
+ ) do |value|
304
+ raise Errors::InvalidSecFetchHeader,
305
+ "Expected sec-fetch-dest to equal document but got #{value.inspect}"
306
+ end
307
+
308
+ session =
309
+ Session.new(
310
+ environment: @environment,
311
+ path: request.path,
312
+ headers: request.headers.to_h.freeze
313
+ )
314
+ body = Async::HTTP::Body::Writable.new
315
+
316
+ headers = {
317
+ "content-type" => "text/html; charset=utf-8",
318
+ "cache" => "no-cache"
319
+ }
320
+
321
+ accept_encodings = request.headers["accept-encoding"].to_s.split(", ")
322
+
323
+ writer =
324
+ if accept_encodings.include?("br")
325
+ headers["content-encoding"] = "br"
326
+ Brotli::Writer.new(body)
327
+ else
328
+ body
329
+ end
330
+
331
+ session.initial_render(writer) => { stylesheets: }
332
+
333
+ headers["link"] = [
334
+ "</__mayu/runtime/#{@environment.init_js}##{session.id}>; rel=preload; as=script; crossorigin=same-origin; fetchpriority=high",
335
+ *stylesheets.map { "<#{_1}>; rel=preload; as=style" }
336
+ ].join(", ")
337
+
338
+ headers["set-cookie"] = set_token_cookie_value(session)
339
+
340
+ @sessions.store(session.id, session)
341
+
342
+ @environment.metrics.session_init_count.increment()
343
+
344
+ Protocol::HTTP::Response[200, headers, body]
345
+ end
346
+
347
+ sig do
348
+ params(session: Session, body: Async::HTTP::Body::Writable).returns(
349
+ Async::Task
350
+ )
351
+ end
352
+ def run_event_stream(session, body:)
353
+ @barrier.async do |task|
354
+ session.activity!
355
+
356
+ stream = EventStream::Writable.new(body)
357
+
358
+ Console.logger.info(self, "Streaming events to session #{session.id}")
359
+
360
+ barrier = Async::Barrier.new
361
+ stop_notification = Async::Notification.new
362
+
363
+ task.async do
364
+ @stop.wait
365
+ stop_notification.signal
366
+ end
367
+
368
+ session_task =
369
+ barrier.async do
370
+ session
371
+ .run do |message|
372
+ case message
373
+ in [event, payload]
374
+ session.log.push(:"session.#{event}", payload)
375
+ end
376
+ end
377
+ .wait
378
+ ensure
379
+ stop_notification.signal
380
+ end
381
+
382
+ ping_task =
383
+ barrier.async do
384
+ loop do
385
+ sleep PING_INTERVAL
386
+ session.log.push(:ping, time_ping_value)
387
+ end
388
+ end
389
+
390
+ message_task =
391
+ barrier.async do |subtask|
392
+ loop { stream.write(session.log.pop.to_a) }
393
+ ensure
394
+ barrier.stop
395
+ end
396
+
397
+ stop_notification.wait
398
+
399
+ barrier.stop
400
+ perform_transfer(session, stream)
401
+ task.stop
402
+ end
403
+ end
404
+
405
+ private
406
+
407
+ sig do
408
+ params(
409
+ headers: Protocol::HTTP::Headers,
410
+ name: String,
411
+ expected_value: String,
412
+ block: T.proc.params(arg0: String).void
413
+ ).void
414
+ end
415
+ def validate_header!(headers, name, expected_value, &block)
416
+ if actual_value = headers[name]
417
+ yield actual_value.to_s unless actual_value.to_s == expected_value
418
+ end
419
+ end
420
+
421
+ sig { params(session_id: String, body: String).returns(Session) }
422
+ def load_session(session_id, body)
423
+ if body.empty?
424
+ return(
425
+ @sessions.fetch(session_id) do
426
+ raise Errors::SessionNotFound, "Session not found: #{session_id}"
427
+ end
428
+ )
429
+ end
430
+
431
+ @environment.message_cipher.load(body) => String => dumped
432
+ session = Session.restore(environment: @environment, dumped:)
433
+ @sessions.store(session.id, session)
434
+ end
435
+
436
+ sig do
437
+ params(
438
+ session: Session,
439
+ stream: EventStream::Writable,
440
+ task: Async::Task
441
+ ).void
442
+ end
443
+ def perform_transfer(session, stream, task: Async::Task.current)
444
+ return if stream.closed?
445
+
446
+ Console.logger.info(self, "Session #{session.id}: Transferring")
447
+
448
+ stream.write(
449
+ EventStream::Message.new(
450
+ :"session.transfer",
451
+ EventStream::Blob.new(
452
+ @environment.message_cipher.dump(
453
+ Session::SerializedSession.dump_session(session)
454
+ )
455
+ )
456
+ ).to_a
457
+ )
458
+
459
+ # Sleep a little bit so that the message
460
+ # gets sent before the body closes...
461
+ # This is not ideal though, it would be better
462
+ # maybe if the client would acknowledge that they
463
+ # have received it?
464
+ sleep 0.1
465
+ stream.close
466
+ end
467
+
468
+ sig { params(request: Protocol::HTTP::Request).returns(String) }
469
+ def get_token_cookie_value(request)
470
+ Array(request.headers["cookie"]).each do |str|
471
+ if match = str.match(/^mayu-token=(\w+)/)
472
+ return match[1].to_s.tap { Session.validate_token!(_1) }
473
+ end
474
+ end
475
+
476
+ raise Errors::CookieNotSet
477
+ end
478
+
479
+ sig { params(session: Session, ttl_seconds: Integer).returns(String) }
480
+ def set_token_cookie_value(session, ttl_seconds: 60)
481
+ expires = Time.now.utc + ttl_seconds
482
+
483
+ [
484
+ "mayu-token=#{session.token}",
485
+ "path=/__mayu/session/#{session.id}/",
486
+ "expires=#{expires.httpdate}",
487
+ "secure",
488
+ "HttpOnly",
489
+ "SameSite=Strict"
490
+ ].join("; ")
491
+ end
492
+ end
493
+ end
494
+ end
@@ -0,0 +1,152 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "async/container/controller"
5
+ require "async/io/shared_endpoint"
6
+ require "async/io/trap"
7
+ require_relative "app"
8
+ require_relative "../metrics"
9
+ require_relative "../app_metrics"
10
+
11
+ module Mayu
12
+ module Server
13
+ class Controller < Async::Container::Controller
14
+ extend T::Sig
15
+
16
+ sig { returns(Async::Container::Generic) }
17
+ def create_container
18
+ Async::Container::Hybrid.new
19
+ end
20
+
21
+ sig do
22
+ params(
23
+ config: Configuration,
24
+ endpoint: Async::HTTP::Endpoint,
25
+ options: T.untyped
26
+ ).void
27
+ end
28
+ def initialize(config:, endpoint:, **options)
29
+ super(**options)
30
+ @config = config
31
+ @endpoint = endpoint
32
+ @interrupt_trap = T.let(Async::IO::Trap.new(:INT), Async::IO::Trap)
33
+ @bound_endpoint = T.let(nil, T.nilable(Async::IO::SharedEndpoint))
34
+ end
35
+
36
+ sig { void }
37
+ def start
38
+ Console.logger.info(self, "Binding to #{@endpoint.url}")
39
+
40
+ @bound_endpoint =
41
+ Async { Async::IO::SharedEndpoint.bound(@endpoint) }.wait
42
+
43
+ super
44
+ end
45
+
46
+ sig { params(timeout: T::Boolean).void }
47
+ def stop(timeout = true)
48
+ super(timeout && 5)
49
+ end
50
+
51
+ sig { params(container: Async::Container::Generic).void }
52
+ def setup(container)
53
+ collector_endpoint = Async::IO::Endpoint.unix("metrics.ipc")
54
+ exporter_endpoint = Async::HTTP::Endpoint.parse("http://[::]:9092")
55
+
56
+ Metrics.start_collect_and_export(
57
+ container,
58
+ collector_endpoint:,
59
+ exporter_endpoint:
60
+ ) { |registry| AppMetrics.setup(registry, instance_id: "Collector") }
61
+
62
+ # TODO: We're waiting for the collector to start.
63
+ # Better make start_collect_and_export block until started.
64
+ sleep 0.2
65
+
66
+ container.run(
67
+ name: "mayu-live server",
68
+ count: @config.server.count,
69
+ threads: @config.server.threads,
70
+ forks: @config.server.forks
71
+ ) do |instance, asd|
72
+ Async do |task|
73
+ interrupt = Async::Notification.new
74
+
75
+ metrics =
76
+ Metrics::Reporter.run(collector_endpoint) do |registry|
77
+ AppMetrics.setup(registry)
78
+ end
79
+
80
+ task.async do
81
+ @interrupt_trap.install!
82
+
83
+ @interrupt_trap.trap { interrupt.signal }
84
+ end
85
+
86
+ environment = Environment.new(@config, metrics)
87
+ app = App.new(environment:)
88
+
89
+ server =
90
+ Async::HTTP::Server.new(
91
+ app,
92
+ @bound_endpoint,
93
+ protocol: Async::HTTP::Protocol::HTTP2,
94
+ scheme: @endpoint.scheme
95
+ )
96
+
97
+ start_hot_swap(environment, app) if @config.server.hot_swap
98
+
99
+ if @config.server.generate_assets
100
+ environment.resources.generate_assets(
101
+ environment.path(:assets),
102
+ concurrency: Async::Container.processor_count,
103
+ forever: true
104
+ )
105
+ end
106
+
107
+ server_task = server.run
108
+
109
+ task.async do
110
+ loop do
111
+ sleep 1
112
+ app.clear_expired_sessions!
113
+ end
114
+ end
115
+
116
+ instance.ready!
117
+
118
+ interrupt.wait
119
+ app.stop
120
+ raise Interrupt
121
+ end
122
+ rescue => e
123
+ Console.logger.error(self, e)
124
+ end
125
+ end
126
+
127
+ sig { params(environment: Environment, app: App, task: Async::Task).void }
128
+ def start_hot_swap(environment, app, task: Async::Task.current)
129
+ task.async do
130
+ if environment.config.use_bundle
131
+ Console.logger.error(
132
+ self,
133
+ "Disabling hot swap because bundle is used"
134
+ )
135
+ return
136
+ end
137
+
138
+ require_relative "../resources/hot_swap"
139
+
140
+ Resources::HotSwap.start(environment.resources) do
141
+ Console.logger.info(
142
+ self,
143
+ Colors.rainbow("Detected code changes, rerendering.")
144
+ )
145
+
146
+ app.rerender
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end