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.
- checksums.yaml +7 -0
- data/COPYING +661 -0
- data/README.md +598 -0
- data/exe/mayu +33 -0
- data/lib/mayu/app_metrics.rb +93 -0
- data/lib/mayu/banner.rb +12 -0
- data/lib/mayu/client/README.md +17 -0
- data/lib/mayu/client/dist/DecompressionStreamPolyfill-3ceba43e.js +1 -0
- data/lib/mayu/client/dist/DecompressionStreamPolyfill-3ceba43e.js.br +0 -0
- data/lib/mayu/client/dist/DecompressionStreamPolyfill-3ceba43e.js.map +1 -0
- data/lib/mayu/client/dist/DecompressionStreamPolyfill-3ceba43e.js.map.br +0 -0
- data/lib/mayu/client/dist/custom-elements/mayu-alert-cd7ad2a4.js +1 -0
- data/lib/mayu/client/dist/custom-elements/mayu-alert-cd7ad2a4.js.map +1 -0
- data/lib/mayu/client/dist/custom-elements/mayu-disconnected-9f349f46.js +1 -0
- data/lib/mayu/client/dist/custom-elements/mayu-disconnected-9f349f46.js.map +1 -0
- data/lib/mayu/client/dist/custom-elements/mayu-exception-63df4e8c.js +1 -0
- data/lib/mayu/client/dist/custom-elements/mayu-exception-63df4e8c.js.map +1 -0
- data/lib/mayu/client/dist/custom-elements/mayu-ping-c498c2a6.js +1 -0
- data/lib/mayu/client/dist/custom-elements/mayu-ping-c498c2a6.js.map +1 -0
- data/lib/mayu/client/dist/custom-elements/mayu-progress-bar-eb3e1ac8.js +1 -0
- data/lib/mayu/client/dist/custom-elements/mayu-progress-bar-eb3e1ac8.js.map +1 -0
- data/lib/mayu/client/dist/entries.json +3 -0
- data/lib/mayu/client/dist/main-4b49dbc4.js +1 -0
- data/lib/mayu/client/dist/main-4b49dbc4.js.br +0 -0
- data/lib/mayu/client/dist/main-4b49dbc4.js.map +1 -0
- data/lib/mayu/client/dist/main-4b49dbc4.js.map.br +0 -0
- data/lib/mayu/client/package.json +39 -0
- data/lib/mayu/client/rollup.config.js +81 -0
- data/lib/mayu/client/src/DecompressionStream.ts +15 -0
- data/lib/mayu/client/src/DecompressionStreamPolyfill.ts +43 -0
- data/lib/mayu/client/src/MimeTypes.ts +4 -0
- data/lib/mayu/client/src/NodeTree.ts +445 -0
- data/lib/mayu/client/src/custom-elements/mayu-alert.html +137 -0
- data/lib/mayu/client/src/custom-elements/mayu-alert.ts +62 -0
- data/lib/mayu/client/src/custom-elements/mayu-disconnected.html +134 -0
- data/lib/mayu/client/src/custom-elements/mayu-disconnected.ts +51 -0
- data/lib/mayu/client/src/custom-elements/mayu-exception.html +79 -0
- data/lib/mayu/client/src/custom-elements/mayu-exception.ts +28 -0
- data/lib/mayu/client/src/custom-elements/mayu-log.html +70 -0
- data/lib/mayu/client/src/custom-elements/mayu-log.ts +42 -0
- data/lib/mayu/client/src/custom-elements/mayu-ping.html +36 -0
- data/lib/mayu/client/src/custom-elements/mayu-ping.ts +53 -0
- data/lib/mayu/client/src/custom-elements/mayu-progress-bar.html +44 -0
- data/lib/mayu/client/src/custom-elements/mayu-progress-bar.ts +40 -0
- data/lib/mayu/client/src/custom-elements/types.d.ts +4 -0
- data/lib/mayu/client/src/global.d.ts +26 -0
- data/lib/mayu/client/src/h.ts +27 -0
- data/lib/mayu/client/src/logger.ts +56 -0
- data/lib/mayu/client/src/main.ts +271 -0
- data/lib/mayu/client/src/serializeEvent.ts +90 -0
- data/lib/mayu/client/src/stream.ts +175 -0
- data/lib/mayu/client/src/types.ts +1 -0
- data/lib/mayu/client/src/utils.ts +71 -0
- data/lib/mayu/client/tsconfig.json +18 -0
- data/lib/mayu/colors.rb +34 -0
- data/lib/mayu/commands/base.rb +22 -0
- data/lib/mayu/commands/build.rb +82 -0
- data/lib/mayu/commands.rb +53 -0
- data/lib/mayu/component/base.rb +177 -0
- data/lib/mayu/component/handler_ref.rb +99 -0
- data/lib/mayu/component/helpers.rb +93 -0
- data/lib/mayu/component/interface.rb +18 -0
- data/lib/mayu/component/wrapper.rb +165 -0
- data/lib/mayu/component.rb +54 -0
- data/lib/mayu/configuration.rb +195 -0
- data/lib/mayu/disable_sorbet.rb +23 -0
- data/lib/mayu/environment.rb +151 -0
- data/lib/mayu/event_stream.rb +158 -0
- data/lib/mayu/fetch.rb +88 -0
- data/lib/mayu/html.rb +53 -0
- data/lib/mayu/html.yaml +767 -0
- data/lib/mayu/message_cipher.rb +172 -0
- data/lib/mayu/message_cipher.test.rb +16 -0
- data/lib/mayu/metrics/collector.rb +161 -0
- data/lib/mayu/metrics/exporter.rb +47 -0
- data/lib/mayu/metrics/reporter.rb +187 -0
- data/lib/mayu/metrics.rb +82 -0
- data/lib/mayu/ref_counter.rb +57 -0
- data/lib/mayu/resources/README.md +14 -0
- data/lib/mayu/resources/asset.rb +71 -0
- data/lib/mayu/resources/assets.rb +76 -0
- data/lib/mayu/resources/dependency_graph.rb +306 -0
- data/lib/mayu/resources/dot_exporter.rb +167 -0
- data/lib/mayu/resources/generators/base.rb +18 -0
- data/lib/mayu/resources/generators/copy_file.rb +26 -0
- data/lib/mayu/resources/generators/image.rb +106 -0
- data/lib/mayu/resources/generators/write_file.rb +39 -0
- data/lib/mayu/resources/hot_swap/file_watcher.rb +69 -0
- data/lib/mayu/resources/hot_swap.rb +46 -0
- data/lib/mayu/resources/mermaid_exporter.rb +210 -0
- data/lib/mayu/resources/registry.rb +190 -0
- data/lib/mayu/resources/resolver/base.rb +32 -0
- data/lib/mayu/resources/resolver/filesystem.rb +94 -0
- data/lib/mayu/resources/resolver/static.rb +27 -0
- data/lib/mayu/resources/resolver.rb +13 -0
- data/lib/mayu/resources/resource.rb +150 -0
- data/lib/mayu/resources/transformers/__test__/css/adjacent_selectors.in.css +3 -0
- data/lib/mayu/resources/transformers/__test__/css/adjacent_selectors.out.css +6 -0
- data/lib/mayu/resources/transformers/__test__/css/attributes.in.css +3 -0
- data/lib/mayu/resources/transformers/__test__/css/attributes.out.css +6 -0
- data/lib/mayu/resources/transformers/__test__/css/composes.in.css +6 -0
- data/lib/mayu/resources/transformers/__test__/css/composes.out.css +10 -0
- data/lib/mayu/resources/transformers/__test__/css/element_selectors.in.css +3 -0
- data/lib/mayu/resources/transformers/__test__/css/element_selectors.out.css +6 -0
- data/lib/mayu/resources/transformers/__test__/css/has.in.css +7 -0
- data/lib/mayu/resources/transformers/__test__/css/has.out.css +10 -0
- data/lib/mayu/resources/transformers/__test__/css/media_queries.in.css +8 -0
- data/lib/mayu/resources/transformers/__test__/css/media_queries.out.css +12 -0
- data/lib/mayu/resources/transformers/__test__/css/pseudo_classes.in.css +5 -0
- data/lib/mayu/resources/transformers/__test__/css/pseudo_classes.out.css +6 -0
- data/lib/mayu/resources/transformers/__test__/haml/README.md +10 -0
- data/lib/mayu/resources/transformers/__test__/haml/case.haml +8 -0
- data/lib/mayu/resources/transformers/__test__/haml/case.rb +15 -0
- data/lib/mayu/resources/transformers/__test__/haml/class_names.haml +13 -0
- data/lib/mayu/resources/transformers/__test__/haml/class_names.rb +26 -0
- data/lib/mayu/resources/transformers/__test__/haml/comments.haml +5 -0
- data/lib/mayu/resources/transformers/__test__/haml/comments.rb +5 -0
- data/lib/mayu/resources/transformers/__test__/haml/css.haml +3 -0
- data/lib/mayu/resources/transformers/__test__/haml/css.rb +11 -0
- data/lib/mayu/resources/transformers/__test__/haml/dashes.haml +3 -0
- data/lib/mayu/resources/transformers/__test__/haml/dashes.rb +11 -0
- data/lib/mayu/resources/transformers/__test__/haml/early_return.haml +4 -0
- data/lib/mayu/resources/transformers/__test__/haml/early_return.rb +9 -0
- data/lib/mayu/resources/transformers/__test__/haml/early_return2.haml +3 -0
- data/lib/mayu/resources/transformers/__test__/haml/early_return2.rb +6 -0
- data/lib/mayu/resources/transformers/__test__/haml/handlers.haml +6 -0
- data/lib/mayu/resources/transformers/__test__/haml/handlers.rb +12 -0
- data/lib/mayu/resources/transformers/__test__/haml/if_else.haml +6 -0
- data/lib/mayu/resources/transformers/__test__/haml/if_else.rb +12 -0
- data/lib/mayu/resources/transformers/__test__/haml/interpolation.haml +8 -0
- data/lib/mayu/resources/transformers/__test__/haml/interpolation.rb +11 -0
- data/lib/mayu/resources/transformers/__test__/haml/object_ref_as_key.haml +1 -0
- data/lib/mayu/resources/transformers/__test__/haml/object_ref_as_key.rb +5 -0
- data/lib/mayu/resources/transformers/__test__/haml/props.haml +4 -0
- data/lib/mayu/resources/transformers/__test__/haml/props.rb +11 -0
- data/lib/mayu/resources/transformers/__test__/haml/slots.haml +5 -0
- data/lib/mayu/resources/transformers/__test__/haml/slots.rb +9 -0
- data/lib/mayu/resources/transformers/__test__/haml/slots_dynamic.haml +3 -0
- data/lib/mayu/resources/transformers/__test__/haml/slots_dynamic.rb +9 -0
- data/lib/mayu/resources/transformers/__test__/haml/slots_fallback.haml +3 -0
- data/lib/mayu/resources/transformers/__test__/haml/slots_fallback.rb +5 -0
- data/lib/mayu/resources/transformers/__test__/haml/spacing.haml +5 -0
- data/lib/mayu/resources/transformers/__test__/haml/spacing.rb +14 -0
- data/lib/mayu/resources/transformers/__test__/haml/spacing2.haml +10 -0
- data/lib/mayu/resources/transformers/__test__/haml/spacing2.rb +11 -0
- data/lib/mayu/resources/transformers/__test__/haml/spacing3.haml +3 -0
- data/lib/mayu/resources/transformers/__test__/haml/spacing3.rb +10 -0
- data/lib/mayu/resources/transformers/css/rouge_lexer.rb +841 -0
- data/lib/mayu/resources/transformers/css.rb +100 -0
- data/lib/mayu/resources/transformers/css.test.rb +87 -0
- data/lib/mayu/resources/transformers/haml.rb +984 -0
- data/lib/mayu/resources/transformers/haml.test.rb +114 -0
- data/lib/mayu/resources/types/README.md +36 -0
- data/lib/mayu/resources/types/base.rb +35 -0
- data/lib/mayu/resources/types/component.rb +198 -0
- data/lib/mayu/resources/types/image.rb +169 -0
- data/lib/mayu/resources/types/javascript.rb +50 -0
- data/lib/mayu/resources/types/nil.rb +23 -0
- data/lib/mayu/resources/types/stylesheet.rb +119 -0
- data/lib/mayu/resources/types/svg.rb +69 -0
- data/lib/mayu/resources/types.rb +37 -0
- data/lib/mayu/routes.rb +170 -0
- data/lib/mayu/routing/builder.rb +108 -0
- data/lib/mayu/routing/matcher.rb +58 -0
- data/lib/mayu/routing/routes.rb +85 -0
- data/lib/mayu/routing.rb +17 -0
- data/lib/mayu/server/app.rb +494 -0
- data/lib/mayu/server/controller.rb +152 -0
- data/lib/mayu/server/errors.rb +110 -0
- data/lib/mayu/server/file_server.rb +140 -0
- data/lib/mayu/server.rb +63 -0
- data/lib/mayu/session.rb +358 -0
- data/lib/mayu/state/README.md +6 -0
- data/lib/mayu/state/action_creator.rb +191 -0
- data/lib/mayu/state/action_wrapper.rb +30 -0
- data/lib/mayu/state/loader.rb +220 -0
- data/lib/mayu/state/store.rb +82 -0
- data/lib/mayu/state.rb +8 -0
- data/lib/mayu/state.test.rb +97 -0
- data/lib/mayu/utils.rb +114 -0
- data/lib/mayu/vdom/children.rb +117 -0
- data/lib/mayu/vdom/component_marshaler.rb +53 -0
- data/lib/mayu/vdom/css_attributes.rb +131 -0
- data/lib/mayu/vdom/descriptor.rb +151 -0
- data/lib/mayu/vdom/descriptor.test.rb +26 -0
- data/lib/mayu/vdom/dom.rb +239 -0
- data/lib/mayu/vdom/h.rb +22 -0
- data/lib/mayu/vdom/id_generator.rb +55 -0
- data/lib/mayu/vdom/interfaces.rb +186 -0
- data/lib/mayu/vdom/marshalling.rb +78 -0
- data/lib/mayu/vdom/reconciliation.rb +205 -0
- data/lib/mayu/vdom/reconciliation.test.rb +56 -0
- data/lib/mayu/vdom/special_elements.rb +108 -0
- data/lib/mayu/vdom/update_context.rb +180 -0
- data/lib/mayu/vdom/vdom.perf.test.rb +146 -0
- data/lib/mayu/vdom/vnode.rb +266 -0
- data/lib/mayu/vdom/vtree.rb +672 -0
- data/lib/mayu/vdom/vtree.test.rb +68 -0
- data/lib/mayu/vdom.rb +8 -0
- data/lib/mayu/vdom.test.rb +73 -0
- data/lib/mayu/version.rb +6 -0
- data/lib/mayu.rb +8 -0
- data/mayu-live.gemspec +70 -0
- 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
|