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,205 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "interfaces"
5
+
6
+ module Mayu
7
+ module VDOM
8
+ module Reconciliation
9
+ class RangeIterator
10
+ def initialize(elements)
11
+ @head = 0
12
+ @tail = elements.length.pred
13
+ @elements = elements
14
+ end
15
+
16
+ def done? = @head > @tail
17
+
18
+ def head = @elements[@head]
19
+ def tail = @elements[@tail]
20
+
21
+ def tail_idx = @tail
22
+
23
+ def next_head! = @head += 1
24
+ def next_tail! = @tail -= 1
25
+
26
+ def to_a = @elements[to_range] || []
27
+ def to_range = @head..@tail
28
+
29
+ def [](idx)
30
+ @elements[idx]
31
+ end
32
+
33
+ def []=(idx, value)
34
+ @elements[idx] = value
35
+ end
36
+ end
37
+
38
+ module Patches
39
+ extend T::Sig
40
+
41
+ class Init < T::Struct
42
+ const :descriptor, Interfaces::Descriptor
43
+ def inspect = "#{self.class.name}(#{descriptor.type.to_s})"
44
+ end
45
+
46
+ class InsertBefore < T::Struct
47
+ const :vnode, Interfaces::VNode
48
+ const :ref, T.nilable(Interfaces::VNode)
49
+
50
+ def inspect =
51
+ "#{self.class.name}(#{vnode.id.inspect}, #{ref&.id.inspect})"
52
+ end
53
+
54
+ class InsertAfter < T::Struct
55
+ const :vnode, Interfaces::VNode
56
+ const :ref, T.nilable(Interfaces::VNode)
57
+
58
+ def inspect =
59
+ "#{self.class.name}(#{vnode.id.inspect}, #{ref&.id.inspect})"
60
+ end
61
+
62
+ class Patch < T::Struct
63
+ const :vnode, Interfaces::VNode
64
+ const :descriptor, Interfaces::Descriptor
65
+ def inspect =
66
+ "#{self.class.name}(#{vnode.id.inspect}, #{descriptor.type.to_s})"
67
+ end
68
+
69
+ class Remove < T::Struct
70
+ const :vnode, Interfaces::VNode
71
+ def inspect = "#{self.class.name}(#{vnode.id.inspect})"
72
+ end
73
+
74
+ Any =
75
+ T.type_alias { T.any(Init, InsertBefore, InsertAfter, Patch, Remove) }
76
+ end
77
+
78
+ class Result < T::Struct
79
+ const :vnodes, T::Array[Interfaces::VNode]
80
+ const :patches, T::Array[Patches::Any]
81
+ end
82
+
83
+ extend T::Sig
84
+
85
+ sig do
86
+ params(
87
+ old_children: T::Array[Interfaces::VNode],
88
+ descriptors: T::Array[Interfaces::Descriptor],
89
+ block:
90
+ T
91
+ .proc
92
+ .params(arg0: Patches::Any)
93
+ .returns(T.nilable(Interfaces::VNode))
94
+ ).returns(Result)
95
+ end
96
+ def self.reconcile(old_children, descriptors, &block)
97
+ # TODO: Make it possible to disable the following check in production:
98
+ Children.check_duplicate_keys(descriptors)
99
+
100
+ grouped = old_children.group_by { _1.descriptor }
101
+
102
+ new_children =
103
+ descriptors
104
+ .map do |descriptor|
105
+ if vnode = grouped[descriptor]&.shift
106
+ yield Patches::Patch.new(vnode:, descriptor:)
107
+ else
108
+ yield Patches::Init.new(descriptor:)
109
+ end
110
+ end
111
+ .compact
112
+
113
+ patches = T.let([], T::Array[Patches::Any])
114
+
115
+ delta_time_ms =
116
+ Mayu::Utils.measure_time do
117
+ patches.concat(diff(old_children, new_children))
118
+
119
+ grouped.values.flatten.each do |removed|
120
+ patches << Patches::Remove.new(vnode: removed)
121
+ end
122
+ end
123
+
124
+ if delta_time_ms > 10
125
+ Console.logger.warn(self, "Diffing took %.3fms" % delta_time_ms)
126
+ end
127
+
128
+ Result.new(vnodes: new_children, patches:)
129
+ end
130
+
131
+ def self.diff(old, new)
132
+ old = old.dup
133
+ new = new.dup
134
+
135
+ old_ids = old.map(&:id).sort
136
+
137
+ iold = RangeIterator.new(old)
138
+ inew = RangeIterator.new(new)
139
+
140
+ ops = []
141
+
142
+ until iold.done? || inew.done?
143
+ iold.next_head! and next unless iold.head
144
+ iold.next_tail! and next unless iold.tail
145
+ inew.next_head! and next unless inew.head
146
+ inew.next_tail! and next unless inew.tail
147
+
148
+ if iold.tail.eql?(inew.tail)
149
+ iold.next_tail!
150
+ inew.next_tail!
151
+ next
152
+ end
153
+
154
+ if iold.head.eql?(inew.head)
155
+ iold.next_head!
156
+ inew.next_head!
157
+ next
158
+ end
159
+
160
+ if iold.head.eql?(inew.tail)
161
+ # Right move
162
+ ops << Patches::InsertAfter.new(vnode: iold.head, ref: iold.tail)
163
+ iold.next_head!
164
+ inew.next_tail!
165
+ next
166
+ end
167
+
168
+ if iold.tail.eql?(inew.head)
169
+ # Left move
170
+ ops << Patches::InsertBefore.new(vnode: iold.tail, ref: iold.head)
171
+ inew.next_head!
172
+ iold.next_tail!
173
+ next
174
+ end
175
+
176
+ if old_index = old.find_index { _1.eql?(inew.head) }
177
+ old[old_index] = nil
178
+ ops << Patches::InsertBefore.new(vnode: inew.head, ref: iold.head)
179
+ inew.next_head!
180
+ next
181
+ end
182
+
183
+ ops << Patches::InsertBefore.new(vnode: inew.head, ref: iold.head)
184
+
185
+ inew.next_head!
186
+ end
187
+
188
+ if iold.done?
189
+ before = new[inew.tail_idx.succ]
190
+
191
+ until inew.done?
192
+ ops << Patches::InsertBefore.new(vnode: inew.head, ref: before)
193
+ inew.next_head!
194
+ end
195
+ elsif inew.done?
196
+ iold.to_a.compact.each do |vnode|
197
+ # ops << Patches::Remove.new(vnode:)
198
+ end
199
+ end
200
+
201
+ ops
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,56 @@
1
+ # typed: true
2
+
3
+ require "minitest/autorun"
4
+ require "test_helper"
5
+ require_relative "../utils"
6
+ require_relative "reconciliation"
7
+ require_relative "descriptor"
8
+ require_relative "h"
9
+
10
+ class Mayu::VDOM::Reconciliation::Test < Minitest::Test
11
+ class VNode < T::Struct
12
+ extend T::Sig
13
+ include Mayu::VDOM::Interfaces::VNode
14
+
15
+ const :id, String, factory: -> { SecureRandom.alphanumeric(8) }
16
+ prop :descriptor, Mayu::VDOM::Descriptor
17
+ end
18
+
19
+ def test_reconciliation
20
+ descriptors = 200.times.map { |i| Mayu::VDOM::H[:li, i.to_s, key: i] }
21
+
22
+ patches = []
23
+
24
+ vnodes = T.let([], T::Array[VNode])
25
+
26
+ p Mayu::Utils.measure_time { vnodes = update(vnodes, descriptors) }
27
+
28
+ assert_equal(vnodes.map(&:descriptor), descriptors)
29
+
30
+ descriptors = descriptors.shuffle
31
+
32
+ p Mayu::Utils.measure_time { vnodes = update(vnodes, descriptors) }
33
+
34
+ descriptors = descriptors.shuffle.slice(0..100).to_a
35
+
36
+ p Mayu::Utils.measure_time { vnodes = update(vnodes, descriptors) }
37
+
38
+ assert_equal(vnodes.map(&:descriptor).map(&:key), descriptors.map(&:key))
39
+ end
40
+
41
+ def update(vnodes, descriptors)
42
+ result =
43
+ Mayu::VDOM::Reconciliation.reconcile(vnodes, descriptors) do
44
+ case _1
45
+ in Mayu::VDOM::Reconciliation::Patches::Init => init
46
+ VNode.new(descriptor: init.descriptor)
47
+ in Mayu::VDOM::Reconciliation::Patches::Patch => patch
48
+ vnode = patch.vnode
49
+ vnode.descriptor = patch.descriptor
50
+ vnode
51
+ end
52
+ end
53
+ p(count: result.patches.length)
54
+ result.vnodes.then { T.cast(_1, T::Array[VNode]) }
55
+ end
56
+ end
@@ -0,0 +1,108 @@
1
+ # typed: strict
2
+
3
+ require_relative "./descriptor"
4
+ require_relative "../component"
5
+
6
+ module Mayu
7
+ module VDOM
8
+ module SpecialElements
9
+ extend T::Sig
10
+
11
+ class Head < Component::Base
12
+ sig { override.returns(T.nilable(VDOM::Descriptor)) }
13
+ def render
14
+ T.unsafe(VDOM::H)[
15
+ :__mayu_head,
16
+ *children,
17
+ VDOM::H[:__mayu_links],
18
+ **props
19
+ ]
20
+ end
21
+ end
22
+
23
+ class Body < Component::Base
24
+ sig { override.returns(T.nilable(VDOM::Descriptor)) }
25
+ def render
26
+ T.unsafe(VDOM::H)[
27
+ :__mayu_body,
28
+ *children,
29
+ VDOM::H[:__mayu_scripts],
30
+ **props
31
+ ]
32
+ end
33
+ end
34
+
35
+ class A < Component::Base
36
+ EXTERNAL_LINK_RE = T.let(%r{\A[a-z0-9]+://}, Regexp)
37
+
38
+ sig { override.returns(T.nilable(VDOM::Descriptor)) }
39
+ def render
40
+ T.unsafe(VDOM::H)[:__mayu_a, *children, **overridden_props]
41
+ end
42
+
43
+ private
44
+
45
+ sig { returns(T::Hash[Symbol, T.untyped]) }
46
+ def overridden_props
47
+ if EXTERNAL_LINK_RE.match?(props[:href] || nil)
48
+ { rel: "noreferrer", external: true, **props }
49
+ elsif !props[:href] || props[:href].to_s.empty?
50
+ props
51
+ else
52
+ { **props, on_click: "Mayu.navigate(event)" }
53
+ end
54
+ end
55
+ end
56
+
57
+ class Select < Component::Base
58
+ class InvalidNestingError < StandardError
59
+ end
60
+
61
+ sig { override.returns(T.nilable(VDOM::Descriptor)) }
62
+ def render
63
+ value = props[:value]
64
+
65
+ options =
66
+ Array(children).flatten.compact.map do |descriptor|
67
+ unless descriptor.type == :option
68
+ raise InvalidNestingError,
69
+ "Only option are valid children for select, you passed #{descriptor.type}"
70
+ end
71
+
72
+ T.unsafe(VDOM::H)[
73
+ descriptor.type,
74
+ *descriptor.children,
75
+ **descriptor.props,
76
+ key: descriptor.key,
77
+ selected: !value.nil? && value == descriptor.props[:value]
78
+ ]
79
+ end
80
+
81
+ T.unsafe(VDOM::H)[:__mayu_select, *options, **props.except(:value)]
82
+ end
83
+ end
84
+
85
+ MAPPINGS =
86
+ T.let(
87
+ {
88
+ head: Head,
89
+ __mayu_head: :head,
90
+ body: Body,
91
+ __mayu_body: :body,
92
+ a: A,
93
+ __mayu_a: :a,
94
+ select: Select,
95
+ __mayu_select: :select
96
+ }.freeze,
97
+ T::Hash[Symbol, Component::ElementType]
98
+ )
99
+
100
+ sig do
101
+ params(type: Component::ElementType).returns(Component::ElementType)
102
+ end
103
+ def self.for_type(type)
104
+ MAPPINGS.fetch(T.unsafe(type), type)
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,180 @@
1
+ # typed: strict
2
+
3
+ module Mayu
4
+ module VDOM
5
+ class UpdateContext
6
+ extend T::Sig
7
+
8
+ sig { returns(T::Array[T.untyped]) }
9
+ attr_reader :patches
10
+
11
+ sig { void }
12
+ def initialize
13
+ @patches = T.let([], T::Array[T.untyped])
14
+ @parents = T.let([], T::Array[VNode])
15
+ @dom_parent_ids = T.let([], T::Array[VNode::Id])
16
+ @stylesheets = T.let(Set.new, T::Set[String])
17
+ end
18
+
19
+ sig { returns(T.untyped) }
20
+ def stylesheet_patch
21
+ return [] if @stylesheets.empty?
22
+
23
+ paths = @stylesheets.to_a.map { "/__mayu/static/#{_1}" }
24
+
25
+ [{ type: :stylesheet, paths: }]
26
+ end
27
+
28
+ sig { returns(T.nilable(VNode)) }
29
+ def parent = @parents.last
30
+
31
+ sig { returns(VNode::Id) }
32
+ def dom_parent_id = @dom_parent_ids.last || "probably root"
33
+
34
+ sig { params(vnode: VNode, blk: T.proc.void).void }
35
+ def enter(vnode, &blk)
36
+ # Sleep so that the fiber yields,
37
+ # so that other things can run..
38
+ sleep(0)
39
+
40
+ dom_parent_id =
41
+ (vnode.descriptor.element? ? vnode.id : vnode.dom_parent_id)
42
+
43
+ @parents.push(vnode)
44
+ @dom_parent_ids.push(dom_parent_id) if dom_parent_id
45
+ yield
46
+ ensure
47
+ @dom_parent_ids.pop if dom_parent_id
48
+ @parents.pop
49
+ end
50
+
51
+ sig do
52
+ params(
53
+ vnode: VNode,
54
+ before: T.nilable(VNode),
55
+ after: T.nilable(VNode)
56
+ ).void
57
+ end
58
+ def insert(vnode, before: nil, after: nil)
59
+ html = vnode.to_html
60
+ ids = vnode.id_tree
61
+
62
+ if before
63
+ add_patch(
64
+ :insert,
65
+ id: vnode.dom_id,
66
+ parent: dom_parent_id,
67
+ before: before.dom_id,
68
+ html:,
69
+ ids:
70
+ )
71
+ elsif after
72
+ add_patch(
73
+ :insert,
74
+ id: vnode.dom_id,
75
+ parent: dom_parent_id,
76
+ after: after.dom_id,
77
+ html:,
78
+ ids:
79
+ )
80
+ else
81
+ add_patch(
82
+ :insert,
83
+ id: vnode.dom_id,
84
+ parent: dom_parent_id,
85
+ html:,
86
+ ids:
87
+ )
88
+ end
89
+ end
90
+
91
+ sig do
92
+ params(
93
+ vnode: VNode,
94
+ before: T.nilable(VNode),
95
+ after: T.nilable(VNode)
96
+ ).void
97
+ end
98
+ def move(vnode, before: nil, after: nil)
99
+ if before
100
+ add_patch(
101
+ :move,
102
+ id: vnode.dom_id,
103
+ parent: vnode.dom_parent_id,
104
+ before: before.dom_id
105
+ )
106
+ elsif after
107
+ add_patch(
108
+ :move,
109
+ id: vnode.dom_id,
110
+ parent: vnode.dom_parent_id,
111
+ after: after.dom_id
112
+ )
113
+ else
114
+ add_patch(:move, id: vnode.dom_id, parent: vnode.dom_parent_id)
115
+ end
116
+ end
117
+
118
+ sig { params(vnode: VNode, attr: String, value: T.nilable(String)).void }
119
+ def css(vnode, attr, value = nil)
120
+ if value
121
+ add_patch(:css, id: vnode.dom_id, attr:, value:)
122
+ else
123
+ add_patch(:css, id: vnode.dom_id, attr:)
124
+ end
125
+ end
126
+
127
+ sig { params(hash: String).void }
128
+ def stylesheet(hash)
129
+ @stylesheets.add(hash)
130
+ end
131
+
132
+ sig { params(vnode: VNode, text: String, append: T::Boolean).void }
133
+ def text(vnode, text, append: false)
134
+ if append
135
+ add_patch(:text, id: vnode.dom_id, append: text)
136
+ else
137
+ add_patch(:text, id: vnode.dom_id, text:)
138
+ end
139
+ end
140
+
141
+ sig { params(vnode: VNode).void }
142
+ def remove(vnode)
143
+ if vnode.component
144
+ if child = vnode.children.first
145
+ return remove(child)
146
+ end
147
+ end
148
+ # puts "\e[31mremove\e[0m #{vnode.key}"
149
+ add_patch(:remove, id: vnode.dom_id, parent: vnode.dom_parent_id)
150
+ end
151
+
152
+ sig { params(vnode: VNode, name: String, value: String).void }
153
+ def set_attribute(vnode, name, value)
154
+ add_patch(:attr, id: vnode.dom_id, name: attr_name(name), value:)
155
+ end
156
+
157
+ sig { params(vnode: VNode, name: String).void }
158
+ def remove_attribute(vnode, name)
159
+ add_patch(:attr, id: vnode.dom_id, name: attr_name(name))
160
+ end
161
+
162
+ private
163
+
164
+ sig { params(type: Symbol, args: T.untyped).void }
165
+ def add_patch(type, **args)
166
+ # puts "\e[35;5mXXXXXX \e[33m#{type}:\e[0m #{args.inspect}"
167
+ @patches.push(args.merge(type:))
168
+ end
169
+
170
+ sig { params(attr: T.any(String, Symbol)).returns(String) }
171
+ def attr_name(attr)
172
+ attr
173
+ .to_s
174
+ .sub(/^on_/, "on")
175
+ .sub(/\Ainitial_value\Z/, "value")
176
+ .tr("_", "-")
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,146 @@
1
+ # typed: false
2
+
3
+ require "minitest/autorun"
4
+ require "test_helper"
5
+ require "async"
6
+ require "rexml/document"
7
+ require "stringio"
8
+ require "ruby-prof"
9
+ require_relative "vtree"
10
+ require_relative "h"
11
+ require_relative "../session"
12
+ require_relative "../commands"
13
+ require_relative "../app_metrics"
14
+
15
+ class Mayu::VDOM::PerformanceTest < Minitest::Test
16
+ H = Mayu::VDOM::H
17
+
18
+ class Item < Mayu::Component::Base
19
+ def render
20
+ H[:li, H[:a, props[:children].to_a, href: props[:path]]]
21
+ end
22
+ end
23
+
24
+ class MyComponent < Mayu::Component::Base
25
+ def self.get_initial_state(**props)
26
+ { page: 0 }
27
+ end
28
+
29
+ def handle_next_page(e)
30
+ update { |state| { page: state[:page].succ } }
31
+ end
32
+
33
+ def render
34
+ per_page = 50
35
+ items = props[:items].slice(state[:page] * per_page, per_page)
36
+
37
+ H[
38
+ :div,
39
+ (H[:button, on_click: handler(:handle_next_page)] unless items.empty?),
40
+ H[:ul, items.map { H[Item, _1, key: _1, path: "/#{_1}"] }]
41
+ ]
42
+ end
43
+ end
44
+
45
+ def test_perf
46
+ items = 2000.times.map { SecureRandom.alphanumeric(5 + rand(10)) }
47
+
48
+ Async do
49
+ vtree = setup_vtree
50
+ app = H[MyComponent, items:]
51
+ vtree.render(app)
52
+ vtree.to_html.tap { |html| print_xml(html) }
53
+
54
+ # https://ruby-prof.github.io/#measurements
55
+ RubyProf.measure_mode = RubyProf::WALL_TIME
56
+ # RubyProf.measure_mode = RubyProf::PROCESS_TIME
57
+ # RubyProf.measure_mode = RubyProf::ALLOCATIONS
58
+ # RubyProf.measure_mode = RubyProf::MEMORY
59
+
60
+ profile = RubyProf::Profile.new(track_allocations: true)
61
+ profile.exclude_methods!(T::Types::Union, :recursively_valid?)
62
+ profile.exclude_methods!(T::Types::FixedArray, :initialize)
63
+ profile.exclude_methods!(T::Props::WeakConstructor, :initialize)
64
+ # profile.exclude_methods!(T::Props::Constructor::DecoratorMethods, :construct_props_without_defaults)
65
+ profile.exclude_methods!(T::Types::TypedEnumerable, :recursively_valid?)
66
+ # profile.exclude_methods!(T::Private::Methods::Signature, :initialize)
67
+
68
+ result =
69
+ profile.profile do
70
+ while handler_ref =
71
+ vtree.instance_variable_get(:@handlers).values.first
72
+ handler_ref.call({})
73
+ update_vtree(vtree)
74
+ end
75
+ end
76
+
77
+ printer = RubyProf::MultiPrinter.new(result)
78
+ path = File.join(Mayu::TestHelper::ROOT, "profile")
79
+ FileUtils.mkdir_p(path)
80
+ printer.print(path:, profile: File.basename(__FILE__, ".*"))
81
+
82
+ vtree.render(app)
83
+ vtree.to_html.tap { |html| print_xml(html) }
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ def update_vtree(vtree)
90
+ ctx = Mayu::VDOM::UpdateContext.new
91
+
92
+ vtree.update_queue.size.times do
93
+ case vtree.update_queue.dequeue
94
+ in Mayu::VDOM::VNode => vnode
95
+ next if vnode.removed?
96
+ vtree.patch(ctx, vnode, vnode.descriptor, lifecycles: false)
97
+ else
98
+ # ok
99
+ end
100
+ end
101
+
102
+ ctx
103
+ end
104
+
105
+ def setup_vtree
106
+ $metrics ||= Mayu::AppMetrics.setup(Prometheus::Client.registry)
107
+ config =
108
+ Mayu::Configuration.from_hash!(
109
+ { "mode" => :test, "root" => "/laiehbaleihf", "secret_key" => "test" }
110
+ )
111
+ environment = Mayu::Environment.new(config, $metrics)
112
+
113
+ environment.instance_eval do
114
+ def load_root(path, headers: {})
115
+ H[:div]
116
+ end
117
+ def match_route(path)
118
+ end
119
+ end
120
+
121
+ session = Mayu::Session.new(environment:, path: "/")
122
+ Mayu::VDOM::VTree.new(session:)
123
+ end
124
+
125
+ def print_xml(source)
126
+ io = StringIO.new
127
+ doc = REXML::Document.new(source)
128
+ formatter = REXML::Formatters::Pretty.new
129
+ formatter.compact = true
130
+ formatter.write(doc, io)
131
+ io.rewind
132
+
133
+ puts(
134
+ io
135
+ .read
136
+ .to_s
137
+ .gsub(/(mayu-id='?)(\d+)/) { "#{$~[1]}\e[1;34m#{$~[2]}\e[0m" }
138
+ .gsub(/(mayu-key='?)(\d+)/) { "#{$~[1]}\e[1;35m#{$~[2]}\e[0m" }
139
+ .gsub(/>(.*?)</) { ">\e[33m#{$~[1]}\e[0m<" }
140
+ )
141
+ end
142
+
143
+ def extract_numbers(source)
144
+ REXML::Document.new(source).get_elements("//li").map(&:text).map(&:to_i)
145
+ end
146
+ end