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,672 @@
1
+ # typed: strict
2
+
3
+ require "async/queue"
4
+ require "nanoid"
5
+ require "benchmark"
6
+ require_relative "../component"
7
+ require_relative "interfaces"
8
+ require_relative "descriptor"
9
+ require_relative "dom"
10
+ require_relative "vnode"
11
+ require_relative "css_attributes"
12
+ require_relative "update_context"
13
+ require_relative "id_generator"
14
+ require_relative "../session"
15
+ require_relative "../ref_counter"
16
+ require_relative "../utils"
17
+ require_relative "./reconciliation"
18
+
19
+ module Mayu
20
+ module VDOM
21
+ class VTree
22
+ extend T::Sig
23
+ include Interfaces::VTree
24
+
25
+ class Updater
26
+ extend T::Sig
27
+
28
+ # This value limits how many updates per second we can make.
29
+ DEFAULT_UPDATES_PER_SECOND = 20
30
+
31
+ sig { params(vtree: VTree, updates_per_second: Integer).void }
32
+ def initialize(vtree, updates_per_second: DEFAULT_UPDATES_PER_SECOND)
33
+ @vtree = vtree
34
+ @updates_per_second = updates_per_second
35
+ end
36
+
37
+ sig do
38
+ params(
39
+ metrics: T.nilable(AppMetrics),
40
+ task: Async::Task,
41
+ block: T.proc.params(arg0: [Symbol, T.untyped]).void
42
+ ).returns(Async::Task)
43
+ end
44
+ def run(metrics: nil, task: Async::Task.current, &block)
45
+ task.async(annotation: "VTree updater") do |task|
46
+ assets = T::Set[String].new
47
+
48
+ loop do
49
+ @vtree.update_queue.wait while @vtree.update_queue.empty?
50
+
51
+ start_at = Time.now
52
+
53
+ update(assets:, metrics:) do |event, payload|
54
+ yield [event, payload]
55
+ end
56
+
57
+ delta_time_ms = (Time.now - start_at) * 1000
58
+
59
+ # TODO: Make this configurable..
60
+ # should also be in the prometheus output
61
+ if delta_time_ms > 50
62
+ Console.logger.warn(
63
+ self,
64
+ "Rendering took %.3fms" % delta_time_ms
65
+ )
66
+ end
67
+
68
+ yield [:update_finished, delta_time_ms:]
69
+
70
+ sleep 1.0 / @updates_per_second
71
+ end
72
+ rescue => e
73
+ puts e.message
74
+ puts e.backtrace
75
+ error = {
76
+ type: e.class.name,
77
+ message: e.message,
78
+ backtrace: e.backtrace
79
+ }
80
+
81
+ yield [:exception, error]
82
+ end
83
+ end
84
+
85
+ sig do
86
+ params(stylesheets: T::Array[String]).returns(T::Array[T.untyped])
87
+ end
88
+ def stylesheet_patch(stylesheets)
89
+ return [] if stylesheets.empty?
90
+
91
+ paths = stylesheets.map { "/__mayu/static/#{_1}" }
92
+
93
+ [{ type: :stylesheet, paths: }]
94
+ end
95
+
96
+ sig do
97
+ params(
98
+ assets: T::Set[String],
99
+ metrics: T.nilable(AppMetrics),
100
+ block: T.proc.params(arg0: [Symbol, T.untyped]).void
101
+ ).void
102
+ end
103
+ def update(assets: T::Set[String].new, metrics: nil, &block)
104
+ ctx = UpdateContext.new
105
+
106
+ @vtree.update_queue.size.times do
107
+ case @vtree.update_queue.dequeue
108
+ in [:replace_root, descriptor]
109
+ @vtree.render(descriptor, ctx:)
110
+ in [:navigate, path]
111
+ yield [:navigate, path]
112
+ in [:action, payload]
113
+ yield [:action, payload]
114
+ in [:exception, error]
115
+ yield [:exception, error]
116
+ in [:pong, timestamp]
117
+ yield [:pong, timestamp]
118
+ in VNode => vnode
119
+ next if vnode.removed?
120
+ next unless vnode.component&.dirty?
121
+
122
+ if metrics
123
+ type = vnode.descriptor.type
124
+ vnode_type =
125
+ if type.respond_to?(:__mayu_resource) &&
126
+ resource = type.__mayu_resource
127
+ resource.path
128
+ else
129
+ type.inspect[0..10].to_s
130
+ end
131
+
132
+ metrics.vnode_patch_times.observe(
133
+ Benchmark.realtime do
134
+ @vtree.patch(ctx, vnode, vnode.descriptor, lifecycles: true)
135
+ end,
136
+ labels: {
137
+ vnode_type:
138
+ }
139
+ )
140
+ else
141
+ @vtree.patch(ctx, vnode, vnode.descriptor, lifecycles: true)
142
+ end
143
+ end
144
+ end
145
+
146
+ stylesheets = []
147
+
148
+ @vtree.assets.each do |asset|
149
+ next if assets.include?(asset)
150
+ next unless asset.end_with?(".css")
151
+ stylesheets.push(asset)
152
+ assets.add(asset)
153
+ end
154
+
155
+ patches = [*stylesheet_patch(stylesheets), *ctx.patches]
156
+ yield [:patch, patches] unless patches.empty?
157
+
158
+ @vtree.cleanup_unused_handlers!
159
+ end
160
+ end
161
+
162
+ sig { override.returns(Session) }
163
+ attr_reader :session
164
+
165
+ sig { returns(Async::Queue) }
166
+ attr_reader :update_queue
167
+ sig { returns(T.nilable(VNode)) }
168
+ attr_reader :root
169
+
170
+ sig { returns(T::Set[String]) }
171
+ attr_reader :assets
172
+
173
+ sig { params(session: Session, task: Async::Task).void }
174
+ def initialize(session:, task: Async::Task.current)
175
+ @root = T.let(nil, T.nilable(VNode))
176
+ @id_generator = T.let(IdGenerator.new, IdGenerator)
177
+ @session = T.let(session, Session)
178
+
179
+ @handlers = T.let({}, T::Hash[String, Component::HandlerRef])
180
+ @handler_counts = T.let(RefCounter.new, RefCounter[String])
181
+
182
+ @update_queue = T.let(Async::Queue.new, Async::Queue)
183
+
184
+ @update_semaphore =
185
+ T.let(Async::Semaphore.new(parent: task), Async::Semaphore)
186
+
187
+ @assets = T.let(Set.new, T::Set[String])
188
+ end
189
+
190
+ sig { returns(T::Array[T.untyped]) }
191
+ def marshal_dump
192
+ [@root, @id_generator, @assets]
193
+ end
194
+
195
+ sig { params(a: T::Array[T.untyped]).void }
196
+ def marshal_load(a)
197
+ @root, @id_generator, @assets = a
198
+ @handlers = {}
199
+ @handler_counts = RefCounter.new
200
+ @update_queue = Async::Queue.new
201
+ @update_semaphore = Async::Semaphore.new
202
+ @assets = Set.new
203
+ @root.instance_variable_set(:@vtree, self)
204
+ end
205
+
206
+ sig do
207
+ params(
208
+ descriptor: Interfaces::Descriptor,
209
+ ctx: UpdateContext,
210
+ lifecycles: T::Boolean
211
+ ).returns(UpdateContext)
212
+ end
213
+ def render(descriptor, ctx: UpdateContext.new, lifecycles: true)
214
+ start_at = Time.now
215
+ @root = patch(ctx, @root, descriptor, lifecycles:)
216
+ ctx
217
+ end
218
+
219
+ sig { params(descriptor: Interfaces::Descriptor).void }
220
+ def replace_root(descriptor)
221
+ @update_queue.enqueue([:replace_root, descriptor])
222
+ end
223
+
224
+ sig { params(handler_id: String, payload: T.untyped).void }
225
+ def handle_callback(handler_id, payload = {})
226
+ case handler_id
227
+ when "ping"
228
+ @update_queue.enqueue([:pong, payload[:timestamp]])
229
+ return
230
+ when "navigate"
231
+ navigate(payload[:path])
232
+ return
233
+ end
234
+
235
+ @handlers
236
+ .fetch(handler_id) do
237
+ raise KeyError, "Handler not found: #{handler_id}"
238
+ end
239
+ .call(payload)
240
+ rescue => e
241
+ puts e.message
242
+ puts e.backtrace
243
+ error = {
244
+ type: e.class.name,
245
+ message: e.message,
246
+ backtrace: e.backtrace
247
+ }
248
+ @update_queue.enqueue([:exception, error])
249
+ end
250
+
251
+ sig { returns(String) }
252
+ def to_html
253
+ @root&.to_html.to_s
254
+ end
255
+
256
+ sig { returns(T.untyped) }
257
+ def id_tree
258
+ @root&.id_tree
259
+ end
260
+
261
+ sig { override.params(vnode: VNode).void }
262
+ def enqueue_update!(vnode)
263
+ component = vnode.component
264
+ return unless component
265
+ return if component.dirty?
266
+
267
+ component.dirty!
268
+ @update_queue.enqueue(vnode)
269
+ end
270
+
271
+ sig { override.returns(IdGenerator::Type) }
272
+ def next_id! = @id_generator.next!
273
+
274
+ sig { override.params(path: String).void }
275
+ def navigate(path)
276
+ @update_queue.enqueue([:navigate, path])
277
+ end
278
+
279
+ sig { override.params(type: Symbol, payload: T.untyped).void }
280
+ def action(type, payload)
281
+ @update_queue.enqueue([:action, { type:, payload: }])
282
+ end
283
+
284
+ sig do
285
+ params(
286
+ ctx: UpdateContext,
287
+ vnode: T.nilable(VNode),
288
+ descriptor: T.nilable(Interfaces::Descriptor),
289
+ lifecycles: T::Boolean
290
+ ).returns(T.nilable(VNode))
291
+ end
292
+ def patch(ctx, vnode, descriptor, lifecycles:)
293
+ unless vnode
294
+ return nil unless descriptor
295
+
296
+ vnode = init_vnode(ctx, descriptor, lifecycles:)
297
+ ctx.insert(vnode)
298
+ return vnode
299
+ end
300
+
301
+ return remove_vnode(ctx, vnode, lifecycles:) unless descriptor
302
+
303
+ if vnode.descriptor.same?(descriptor)
304
+ patch_vnode(ctx, vnode, descriptor, lifecycles:)
305
+ else
306
+ remove_vnode(ctx, vnode, lifecycles:)
307
+ vnode = init_vnode(ctx, descriptor, lifecycles:)
308
+ ctx.insert(vnode)
309
+ return vnode
310
+ end
311
+ end
312
+
313
+ sig { void }
314
+ def cleanup_unused_handlers!
315
+ @handlers.delete_if do |id, handler|
316
+ if @handler_counts.count(id).zero?
317
+ Console.logger.warn(self, "Removing handler #{id}")
318
+ true
319
+ end
320
+ end
321
+ end
322
+
323
+ private
324
+
325
+ sig do
326
+ params(
327
+ ctx: UpdateContext,
328
+ vnode: VNode,
329
+ descriptor: Interfaces::Descriptor,
330
+ lifecycles: T::Boolean
331
+ ).returns(VNode)
332
+ end
333
+ def patch_vnode(ctx, vnode, descriptor, lifecycles:)
334
+ unless vnode.descriptor.same?(descriptor)
335
+ raise "Can not patch different types!"
336
+ end
337
+
338
+ if component = vnode.component
339
+ if component.should_update?(descriptor.props, component.next_state)
340
+ vnode.descriptor = descriptor
341
+ prev_props, prev_state = component.props, component.state
342
+ component.props = descriptor.props
343
+ component.state = component.next_state.clone
344
+
345
+ descriptors = clean_children(component.render, parent: descriptor)
346
+
347
+ ctx.enter(vnode) do
348
+ vnode.children =
349
+ update_children(
350
+ ctx,
351
+ vnode.children.compact,
352
+ descriptors,
353
+ lifecycles:
354
+ )
355
+ end
356
+
357
+ update_stylesheet(ctx, component)
358
+
359
+ component.did_update(prev_props, prev_state) if lifecycles
360
+ end
361
+
362
+ return vnode
363
+ end
364
+
365
+ type = descriptor.type
366
+
367
+ if type.is_a?(Proc)
368
+ vnode.descriptor = descriptor
369
+ descriptors = Array(type.call(**descriptor.props)).compact
370
+
371
+ ctx.enter(vnode) do
372
+ vnode.children =
373
+ update_children(
374
+ ctx,
375
+ vnode.children.compact,
376
+ descriptors,
377
+ lifecycles:
378
+ )
379
+ end
380
+
381
+ return vnode
382
+ end
383
+
384
+ return vnode if vnode.descriptor == descriptor
385
+
386
+ if descriptor.text?
387
+ unless vnode.descriptor.text == descriptor.text
388
+ if append = append_part(vnode.descriptor.text, descriptor.text)
389
+ ctx.text(vnode, append, append: true)
390
+ else
391
+ ctx.text(vnode, descriptor.text)
392
+ end
393
+ vnode.descriptor = descriptor
394
+ return vnode
395
+ end
396
+ else
397
+ if vnode.descriptor.has_children? && descriptor.has_children?
398
+ if vnode.descriptor.children != descriptor.children
399
+ ctx.enter(vnode) do
400
+ vnode.children =
401
+ update_children(
402
+ ctx,
403
+ vnode.children,
404
+ descriptor.children.to_a,
405
+ lifecycles:
406
+ )
407
+ end
408
+ end
409
+ elsif descriptor.has_children?
410
+ ctx.enter(vnode) do
411
+ vnode.children =
412
+ clean_children(
413
+ descriptor.children.to_a,
414
+ parent: descriptor
415
+ ).map do
416
+ init_vnode(ctx, _1, lifecycles:).tap do |child|
417
+ ctx.insert(child)
418
+ end
419
+ end
420
+ end
421
+ elsif vnode.children.length > 0
422
+ ctx.enter(vnode) do
423
+ vnode.children.each { remove_vnode(ctx, _1, lifecycles:) }
424
+ end
425
+ vnode.children = []
426
+ elsif vnode.descriptor.text?
427
+ ctx.text(vnode, "")
428
+ else
429
+ # Everything seems to be exactly the same
430
+ end
431
+ end
432
+
433
+ update_handlers(vnode.props, descriptor.props)
434
+
435
+ update_attributes(
436
+ ctx,
437
+ vnode,
438
+ Utils.flatten_props(vnode.props),
439
+ Utils.flatten_props(descriptor.props)
440
+ )
441
+
442
+ vnode.descriptor = descriptor
443
+
444
+ vnode
445
+ end
446
+
447
+ sig do
448
+ params(
449
+ ctx: UpdateContext,
450
+ vnodes: T::Array[VNode],
451
+ lifecycles: T::Boolean
452
+ ).returns(NilClass)
453
+ end
454
+ def remove_vnodes(ctx, vnodes, lifecycles:)
455
+ vnodes.each { |vnode| remove_vnode(ctx, vnode, lifecycles:) }
456
+ nil
457
+ end
458
+
459
+ sig { params(ctx: UpdateContext, component: Component::Wrapper).void }
460
+ def update_stylesheet(ctx, component)
461
+ # TODO: Make this more generic..
462
+ # This only works with CSS right now.
463
+ # Images could also be preloaded.
464
+ # https://web.dev/preload-responsive-images/
465
+ component.assets.each { |asset| @assets.add(asset) }
466
+ end
467
+
468
+ sig do
469
+ params(
470
+ ctx: UpdateContext,
471
+ descriptor: Interfaces::Descriptor,
472
+ lifecycles: T::Boolean,
473
+ nested: T::Boolean
474
+ ).returns(VNode)
475
+ end
476
+ def init_vnode(ctx, descriptor, lifecycles:, nested: false)
477
+ vnode = VNode.build(self, ctx.dom_parent_id, descriptor)
478
+
479
+ component = vnode.init_component
480
+
481
+ children =
482
+ if component
483
+ Array(component.render).compact
484
+ else
485
+ descriptor.props[:children].to_a
486
+ end
487
+
488
+ update_stylesheet(ctx, component) if component
489
+ # puts "\e[32mInitializing vnode #{vnode.id} #{vnode.descriptor.type} with #{children.length} children\e[0m"
490
+
491
+ unless children.empty?
492
+ ctx.enter(vnode) do
493
+ vnode.children =
494
+ clean_children(children, parent: descriptor).map do
495
+ init_vnode(ctx, _1, lifecycles:, nested: true)
496
+ end
497
+ end
498
+ end
499
+
500
+ vnode.component&.mount if lifecycles
501
+
502
+ update_handlers({}, vnode.props)
503
+
504
+ vnode
505
+ end
506
+
507
+ EMPTY_HASH = T.let({}.freeze, T::Hash[T.untyped, T.untyped])
508
+
509
+ sig do
510
+ params(
511
+ ctx: UpdateContext,
512
+ vnode: VNode,
513
+ lifecycles: T::Boolean,
514
+ patch: T::Boolean
515
+ ).returns(NilClass)
516
+ end
517
+ def remove_vnode(ctx, vnode, lifecycles:, patch: true)
518
+ # puts "\e[31mRemoving vnode #{vnode.id} #{vnode.descriptor.type}\e[0m"
519
+
520
+ vnode.component&.unmount if lifecycles
521
+ vnode.remove!
522
+ ctx.remove(vnode) if patch
523
+ vnode.children.map { remove_vnode(ctx, _1, lifecycles:, patch: false) }
524
+ update_handlers(vnode.props, EMPTY_HASH)
525
+ nil
526
+ end
527
+
528
+ sig do
529
+ params(vnode: VNode, descriptor: Interfaces::Descriptor).returns(
530
+ T::Boolean
531
+ )
532
+ end
533
+ def same?(vnode, descriptor)
534
+ vnode.descriptor.same?(descriptor)
535
+ end
536
+
537
+ sig do
538
+ params(
539
+ ctx: UpdateContext,
540
+ vnodes: T::Array[VNode],
541
+ descriptors: T::Array[Interfaces::Descriptor],
542
+ lifecycles: T::Boolean
543
+ ).returns(T::Array[VNode])
544
+ end
545
+ def update_children(ctx, vnodes, descriptors, lifecycles:)
546
+ initialized = T.let([], T::Array[VNode::Id])
547
+
548
+ result =
549
+ Reconciliation.reconcile(vnodes, descriptors) do
550
+ case _1
551
+ in Reconciliation::Patches::Init => init
552
+ vnode = init_vnode(ctx, init.descriptor, lifecycles:)
553
+ initialized.push(vnode.id)
554
+ vnode
555
+ in Reconciliation::Patches::Patch => patch
556
+ patch_vnode(ctx, patch.vnode, patch.descriptor, lifecycles:)
557
+ end
558
+ end
559
+
560
+ result.patches.each do |patch|
561
+ case patch
562
+ in Reconciliation::Patches::InsertBefore => insert
563
+ if initialized.delete(insert.vnode.id)
564
+ ctx.insert(insert.vnode, before: insert.ref)
565
+ else
566
+ ctx.move(insert.vnode, before: insert.ref)
567
+ end
568
+ nil
569
+ in Reconciliation::Patches::InsertAfter => insert
570
+ if initialized.delete(insert.vnode.id)
571
+ ctx.insert(insert.vnode, after: insert.ref)
572
+ else
573
+ ctx.move(insert.vnode, after: insert.ref)
574
+ end
575
+ nil
576
+ in Reconciliation::Patches::Remove => remove
577
+ remove_vnode(ctx, remove.vnode, lifecycles:)
578
+ nil
579
+ end
580
+ end
581
+
582
+ T.cast(result.vnodes, T::Array[VNode])
583
+ end
584
+
585
+ sig do
586
+ params(old_props: Component::Props, new_props: Component::Props).void
587
+ end
588
+ def update_handlers(old_props, new_props)
589
+ old_handlers = old_props.keys.select { _1.start_with?("on") }
590
+ new_handlers = new_props.keys.select { _1.start_with?("on") }
591
+
592
+ # FIXME: If the same handler id is used somewhere else,
593
+ # it will be cleared too. The id needs to include the attribute
594
+ # to be unique. Then we can also remove @handler_counts.
595
+ removed_handlers = old_handlers - new_handlers
596
+
597
+ old_props
598
+ .values_at(*T.unsafe(removed_handlers))
599
+ .select { _1.is_a?(Component::HandlerRef) }
600
+ .each { |handler| @handler_counts.release(handler.id) }
601
+
602
+ new_props
603
+ .values_at(*T.unsafe(new_handlers))
604
+ .select { _1.is_a?(Component::HandlerRef) }
605
+ .each do |handler|
606
+ @handlers[handler.id] = handler
607
+ @handler_counts.acquire!(handler.id)
608
+ end
609
+ end
610
+
611
+ sig do
612
+ params(
613
+ ctx: UpdateContext,
614
+ vnode: VNode,
615
+ old_props: Component::Props,
616
+ new_props: Component::Props
617
+ ).void
618
+ end
619
+ def update_attributes(ctx, vnode, old_props, new_props)
620
+ removed = old_props.keys - new_props.keys - [:children]
621
+
622
+ new_props.each do |attr, value|
623
+ next if attr == :children
624
+ next if attr == :slot
625
+
626
+ old_value = old_props[attr]
627
+
628
+ next if value == old_props[attr]
629
+
630
+ removed.push(attr) and next unless value
631
+ removed.push(attr) and next if value == ""
632
+
633
+ if attr == :style && old_value.is_a?(Hash) && value.is_a?(Hash)
634
+ CSSAttributes.new(**old_value).patch(
635
+ ctx,
636
+ vnode,
637
+ CSSAttributes.new(**value)
638
+ )
639
+ next
640
+ end
641
+
642
+ if value == true
643
+ ctx.set_attribute(vnode, attr.to_s, attr.to_s)
644
+ else
645
+ ctx.set_attribute(vnode, attr.to_s, value.to_s)
646
+ end
647
+ end
648
+
649
+ removed.uniq.each { |attr| ctx.remove_attribute(vnode, attr.to_s) }
650
+ end
651
+
652
+ sig { params(str1: String, str2: String).returns(T.nilable(String)) }
653
+ def append_part(str1, str2)
654
+ return nil if str1.strip.empty? || str1.length >= str2.length
655
+ return nil unless str2.slice(0...str1.length) == str1
656
+ str2.slice(str1.length..-1)
657
+ end
658
+
659
+ sig do
660
+ params(
661
+ children: Component::Children,
662
+ parent: Interfaces::Descriptor
663
+ ).returns(T::Array[Interfaces::Descriptor])
664
+ end
665
+ def clean_children(children, parent:)
666
+ children
667
+ .then { Descriptor::Factory.clean(_1, parent_type: parent) }
668
+ .then { Descriptor::Factory.add_comments_between_texts(_1) }
669
+ end
670
+ end
671
+ end
672
+ end