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,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
|
data/lib/mayu/server.rb
ADDED
@@ -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
|
data/lib/mayu/session.rb
ADDED
@@ -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
|