scarpe-wasm 0.1.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 (48) hide show
  1. checksums.yaml +7 -0
  2. data/CODE_OF_CONDUCT.md +84 -0
  3. data/Gemfile +13 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +39 -0
  6. data/Rakefile +12 -0
  7. data/lib/scarpe/wasm/alert.rb +65 -0
  8. data/lib/scarpe/wasm/app.rb +107 -0
  9. data/lib/scarpe/wasm/arc.rb +54 -0
  10. data/lib/scarpe/wasm/background.rb +27 -0
  11. data/lib/scarpe/wasm/border.rb +24 -0
  12. data/lib/scarpe/wasm/button.rb +50 -0
  13. data/lib/scarpe/wasm/check.rb +29 -0
  14. data/lib/scarpe/wasm/control_interface.rb +147 -0
  15. data/lib/scarpe/wasm/control_interface_test.rb +234 -0
  16. data/lib/scarpe/wasm/dimensions.rb +22 -0
  17. data/lib/scarpe/wasm/document_root.rb +8 -0
  18. data/lib/scarpe/wasm/edit_box.rb +44 -0
  19. data/lib/scarpe/wasm/edit_line.rb +43 -0
  20. data/lib/scarpe/wasm/flow.rb +24 -0
  21. data/lib/scarpe/wasm/font.rb +36 -0
  22. data/lib/scarpe/wasm/html.rb +108 -0
  23. data/lib/scarpe/wasm/image.rb +41 -0
  24. data/lib/scarpe/wasm/line.rb +35 -0
  25. data/lib/scarpe/wasm/link.rb +29 -0
  26. data/lib/scarpe/wasm/list_box.rb +50 -0
  27. data/lib/scarpe/wasm/para.rb +90 -0
  28. data/lib/scarpe/wasm/radio.rb +34 -0
  29. data/lib/scarpe/wasm/shape.rb +68 -0
  30. data/lib/scarpe/wasm/slot.rb +81 -0
  31. data/lib/scarpe/wasm/spacing.rb +41 -0
  32. data/lib/scarpe/wasm/span.rb +66 -0
  33. data/lib/scarpe/wasm/stack.rb +24 -0
  34. data/lib/scarpe/wasm/star.rb +61 -0
  35. data/lib/scarpe/wasm/subscription_item.rb +50 -0
  36. data/lib/scarpe/wasm/text_widget.rb +30 -0
  37. data/lib/scarpe/wasm/version.rb +7 -0
  38. data/lib/scarpe/wasm/video.rb +42 -0
  39. data/lib/scarpe/wasm/wasm_calls.rb +118 -0
  40. data/lib/scarpe/wasm/wasm_local_display.rb +94 -0
  41. data/lib/scarpe/wasm/web_wrangler.rb +679 -0
  42. data/lib/scarpe/wasm/webview_relay_display.rb +220 -0
  43. data/lib/scarpe/wasm/widget.rb +228 -0
  44. data/lib/scarpe/wasm/wv_display_worker.rb +75 -0
  45. data/lib/scarpe/wasm.rb +46 -0
  46. data/scarpe-wasm.gemspec +39 -0
  47. data/sig/scarpe/wasm.rbs +6 -0
  48. metadata +92 -0
@@ -0,0 +1,679 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+
5
+ # WebWrangler operates in multiple phases: setup and running.
6
+
7
+ # After creation, it starts in setup mode, and you can
8
+ # use setup-mode callbacks.
9
+
10
+ class Scarpe
11
+ class WebWrangler
12
+ include Scarpe::Log
13
+
14
+ attr_reader :is_running
15
+ attr_reader :is_terminated
16
+ attr_reader :heartbeat # This is the heartbeat duration in seconds, usually fractional
17
+ attr_reader :control_interface
18
+
19
+ # This error indicates a problem when running ConfirmedEval
20
+ class JSEvalError < Scarpe::Error
21
+ def initialize(data)
22
+ @data = data
23
+ super(data[:msg] || (self.class.name + "!"))
24
+ end
25
+ end
26
+
27
+ # We got an error running the supplied JS code string in confirmed_eval
28
+ class JSRuntimeError < JSEvalError
29
+ end
30
+
31
+ # The code timed out for some reason
32
+ class JSTimeoutError < JSEvalError
33
+ end
34
+
35
+ # We got weird or nonsensical results that seem like an error on WebWrangler's part
36
+ class InternalError < JSEvalError
37
+ end
38
+
39
+ # This is the JS function name for eval results
40
+ EVAL_RESULT = "scarpeAsyncEvalResult"
41
+
42
+ # Allow a half-second for wasm to finish our JS eval before we decide it's not going to
43
+ EVAL_DEFAULT_TIMEOUT = 1.5
44
+
45
+ def initialize(title:, width:, height:, resizable: false, debug: false, heartbeat: 0.1)
46
+ log_init("WASM::WebWrangler")
47
+
48
+ @log.debug("Creating WebWrangler...")
49
+
50
+ # For now, always allow inspect element
51
+ @wasm = Scarpe::WASMInterops.new
52
+ #@wasm = Scarpe::LoggedWrapper.new(@wasm, "wasmAPI") if debug
53
+ @init_refs = {} # Inits don't go away so keep a reference to them to prevent GC
54
+
55
+ @title = title
56
+ @width = width
57
+ @height = height
58
+ @resizable = resizable
59
+ @heartbeat = heartbeat
60
+
61
+ # Better to have a single setInterval than many when we don't care too much
62
+ # about the timing.
63
+ @heartbeat_handlers = []
64
+
65
+ # Need to keep track of which wasm Javascript evals are still pending,
66
+ # what handlers to call when they return, etc.
67
+ @pending_evals = {}
68
+ @eval_counter = 0
69
+
70
+ @dom_wrangler = DOMWrangler.new(self)
71
+
72
+ # bind("puts") do |*args|
73
+ # puts(*args)
74
+ # end
75
+
76
+ @wasm.bind(EVAL_RESULT) do |*results|
77
+ receive_eval_result(*results)
78
+ end
79
+
80
+ # Ruby receives scarpeHeartbeat messages via the window library's main loop.
81
+ # So this is a way for Ruby to be notified periodically, in time with that loop.
82
+ @wasm.bind("scarpeHeartbeat") do
83
+ # return unless @wasm # I think GTK+ may continue to deliver events after shutdown
84
+
85
+ # periodic_js_callback
86
+ @heartbeat_handlers.each(&:call)
87
+ @control_interface.dispatch_event(:heartbeat)
88
+ end
89
+ js_interval = (heartbeat.to_f * 1_000.0).to_i
90
+ @wasm.init("setInterval(scarpeHeartbeat,#{js_interval})")
91
+ end
92
+
93
+ # Shorter name for better stack trace messages
94
+ def inspect
95
+ "Scarpe::WebWrangler:#{object_id}"
96
+ end
97
+
98
+ attr_writer :control_interface
99
+
100
+ ### Setup-mode Callbacks
101
+
102
+ def bind(name, &block)
103
+ raise "App is running, javascript binding no longer works because it uses wasm init!" if @is_running
104
+
105
+ @wasm.bind(name, &block)
106
+ end
107
+
108
+ def init_code(name, &block)
109
+ raise "App is running, javascript init no longer works!" if @is_running
110
+
111
+ # Save a reference to the init string so that it goesn't get GC'd
112
+ code_str = "#{name}();"
113
+ @init_refs[name] = code_str
114
+
115
+ bind(name, &block)
116
+ @wasm.init(code_str)
117
+ end
118
+
119
+ # Run the specified code periodically, every "interval" seconds.
120
+ # If interface is unspecified, run per-heartbeat, which is very
121
+ # slightly more efficient.
122
+ def periodic_code(name, interval = heartbeat, &block)
123
+ if interval == heartbeat
124
+ @heartbeat_handlers << block
125
+ else
126
+ if @is_running
127
+ # I *think* we need to use init because we want this done for every
128
+ # new window. But will there ever be a new page/window? Can we just
129
+ # use eval instead of init to set up a periodic handler and call it
130
+ # good?
131
+ raise "App is running, can't set up new periodic handlers with init!"
132
+ end
133
+
134
+ js_interval = (interval.to_f * 1_000.0).to_i
135
+ code_str = "setInterval(#{name}, #{js_interval});"
136
+ @init_refs[name] = code_str
137
+
138
+ bind(name, &block)
139
+ @wasm.init(code_str)
140
+ end
141
+ end
142
+
143
+ # Running callbacks
144
+
145
+ # js_eventually is a simple JS evaluation. On syntax error, nothing happens.
146
+ # On runtime error, execution stops at the error with no further
147
+ # effect or notification. This is rarely what you want.
148
+ # The js_eventually code is run asynchronously, returning neither error
149
+ # nor value.
150
+ #
151
+ # This method does *not* return a promise, and there is no way to track
152
+ # its progress or its success or failure.
153
+ def js_eventually(code)
154
+ raise "WebWrangler isn't running, eval doesn't work!" unless @is_running
155
+
156
+ # @log.warning "Deprecated: please do NOT use js_eventually, it's basically never what you want!" unless ENV["CI"]
157
+
158
+ @wasm.eval(code)
159
+ end
160
+
161
+ # Eval a chunk of JS code asynchronously. This method returns a
162
+ # promise which will be fulfilled or rejected after the JS executes
163
+ # or times out.
164
+ #
165
+ # Note that we *both* care whether the JS has finished after it was
166
+ # scheduled *and* whether it ever got scheduled at all. If it
167
+ # depends on tasks that never fulfill or reject then it may wait
168
+ # in limbo, potentially forever.
169
+ #
170
+ # Right now we can't/don't handle arguments from previous fulfilled
171
+ # promises. To do that, we'd probably need to know we were passing
172
+ # in a JS function.
173
+ EVAL_OPTS = [:timeout, :wait_for]
174
+ def eval_js_async(code, opts = {})
175
+ # bad_opts = opts.keys - EVAL_OPTS
176
+ # raise("Bad options given to eval_with_handler! #{bad_opts.inspect}") unless bad_opts.empty?
177
+
178
+ # unless @is_running
179
+ # raise "WebWrangler isn't running, so evaluating JS won't work!"
180
+ # end
181
+
182
+ # this_eval_serial = @eval_counter
183
+ # @eval_counter += 1
184
+
185
+ # @pending_evals[this_eval_serial] = {
186
+ # id: this_eval_serial,
187
+ # code: code,
188
+ # start_time: Time.now,
189
+ # timeout_if_not_scheduled: Time.now + EVAL_DEFAULT_TIMEOUT,
190
+ # }
191
+
192
+ # # We'll need this inside the promise-scheduling block
193
+ # pending_evals = @pending_evals
194
+ # timeout = opts[:timeout] || EVAL_DEFAULT_TIMEOUT
195
+
196
+ # promise = Scarpe::Promise.new(parents: (opts[:wait_for] || [])) do
197
+ # # Are we mid-shutdown?
198
+ # if @wasm
199
+ # wrapped_code = WebWrangler.js_wrapped_code(code, this_eval_serial)
200
+
201
+ # # We've been scheduled!
202
+ # t_now = Time.now
203
+ # # Hard to be sure wasm keeps a proper reference to this, so we will
204
+ # pending_evals[this_eval_serial][:wrapped_code] = wrapped_code
205
+
206
+ # pending_evals[this_eval_serial][:scheduled_time] = t_now
207
+ # pending_evals[this_eval_serial].delete(:timeout_if_not_scheduled)
208
+
209
+ # pending_evals[this_eval_serial][:timeout_if_not_finished] = t_now + timeout
210
+ # @wasm.eval(wrapped_code)
211
+ # @log.debug("Scheduled JS: (#{this_eval_serial})\n#{wrapped_code}")
212
+ # else
213
+ # # We're mid-shutdown. No more scheduling things.
214
+ # end
215
+ # end
216
+
217
+ # @pending_evals[this_eval_serial][:promise] = promise
218
+ # @pending_evals[this_eval_serial][:promise].await
219
+ # promise
220
+ js_eventually(code)
221
+ end
222
+
223
+ def self.js_wrapped_code(code, eval_id)
224
+ <<~JS_CODE
225
+ (function() {
226
+ var code_string = #{JSON.dump code};
227
+ try {
228
+ result = eval(code_string);
229
+ #{EVAL_RESULT}("success", #{eval_id}, result);
230
+ } catch(error) {
231
+ #{EVAL_RESULT}("error", #{eval_id}, error.message);
232
+ }
233
+ })();
234
+ JS_CODE
235
+ end
236
+
237
+ private
238
+
239
+ # def periodic_js_callback
240
+ # time_out_eval_results
241
+ # end
242
+
243
+ def receive_eval_result(r_type, id, val)
244
+ entry = @pending_evals.delete(id)
245
+ unless entry
246
+ raise "Received an eval result for a nonexistent ID #{id.inspect}!"
247
+ end
248
+
249
+ @log.debug("Got JS value: #{r_type} / #{id} / #{val.inspect}")
250
+
251
+ # promise = entry[:promise]
252
+
253
+ # case r_type
254
+ # when "success"
255
+ # promise.fulfilled!(val)
256
+ # when "error"
257
+ # promise.rejected! JSRuntimeError.new(
258
+ # msg: "JS runtime error: #{val.inspect}!",
259
+ # code: entry[:code],
260
+ # ret_value: val,
261
+ # )
262
+ # else
263
+ # promise.rejected! InternalError.new(
264
+ # msg: "JS eval internal error! r_type: #{r_type.inspect}",
265
+ # code: entry[:code],
266
+ # ret_value: val,
267
+ # )
268
+ # end
269
+ end
270
+
271
+ # TODO: would be good to keep 'tombstone' results for awhile after timeout, maybe up to around a minute,
272
+ # so we can detect if we're timing things out and then having them return successfully after a delay.
273
+ # Then we could adjust the timeouts. We could also check if later serial numbers have returned, and time
274
+ # out earlier serial numbers... *if* we're sure wasm will always execute JS evals in order.
275
+ # This all adds complexity, though. For now, do timeouts on a simple max duration.
276
+ # def time_out_eval_results
277
+ # t_now = Time.now
278
+ # timed_out_from_scheduling = @pending_evals.keys.select do |id|
279
+ # t = @pending_evals[id][:timeout_if_not_scheduled]
280
+ # t && t_now >= t
281
+ # end
282
+ # timed_out_from_finish = @pending_evals.keys.select do |id|
283
+ # t = @pending_evals[id][:timeout_if_not_finished]
284
+ # t && t_now >= t
285
+ # end
286
+ # timed_out_from_scheduling.each do |id|
287
+ # @log.debug("JS timed out because it was never scheduled: (#{id}) #{@pending_evals[id][:code].inspect}")
288
+ # end
289
+ # timed_out_from_finish.each do |id|
290
+ # @log.debug("JS timed out because it never finished: (#{id}) #{@pending_evals[id][:code].inspect}")
291
+ # end
292
+
293
+ # # A plus *should* be fine since nothing should ever be on both lists. But let's be safe.
294
+ # timed_out_ids = timed_out_from_scheduling | timed_out_from_finish
295
+
296
+ # timed_out_ids.each do |id|
297
+ # @log.error "Timing out JS eval! #{@pending_evals[id][:code]}"
298
+ # entry = @pending_evals.delete(id)
299
+ # err = JSTimeoutError.new(msg: "JS timeout error!", code: entry[:code], ret_value: nil)
300
+ # entry[:promise].rejected!(err)
301
+ # end
302
+ # end
303
+
304
+ public
305
+
306
+ # After setup, we call run to go to "running" mode.
307
+ # No more setup callbacks, only running callbacks.
308
+
309
+ def run
310
+ @log.debug("Run...")
311
+
312
+ # From wasm:
313
+ # 0 - Width and height are default size
314
+ # 1 - Width and height are minimum bonds
315
+ # 2 - Width and height are maximum bonds
316
+ # 3 - Window size can not be changed by a user
317
+ hint = @resizable ? 0 : 3
318
+
319
+ @wasm.set_title(@title)
320
+ @wasm.set_size(@width, @height, hint)
321
+ @wasm.navigate("data:text/html, #{empty}")
322
+
323
+ monkey_patch_console(@wasm)
324
+
325
+ @is_running = true
326
+ @wasm.run
327
+ end
328
+
329
+ def destroy
330
+ @log.debug("Destroying WebWrangler...")
331
+ @log.debug(" (WebWrangler was already terminated)") if @is_terminated
332
+ @log.debug(" (WebWrangler was already destroyed)") unless @wasm
333
+ if @wasm && !@is_terminated
334
+ @bindings = {}
335
+ @wasm.terminate
336
+ @is_terminated = true
337
+ end
338
+ end
339
+
340
+ private
341
+
342
+ # TODO: can this be an init()?
343
+ def monkey_patch_console(window)
344
+ # this forwards all console.log/info/error/warn calls also
345
+ # to the terminal that is running the scarpe app
346
+ # window.eval("
347
+ # function patchConsole(fn) {
348
+ # const original = console[fn];
349
+ # console[fn] = function(...args) {
350
+ # original(...args);
351
+ # puts(...args);
352
+ # }
353
+ # };
354
+ # patchConsole('log');
355
+ # patchConsole('info');
356
+ # patchConsole('error');
357
+ # patchConsole('warn');
358
+ # ")
359
+ end
360
+
361
+ def empty
362
+ html = <<~HTML
363
+ <html>
364
+ <head id='head-wvroot'>
365
+ <style id='style-wvroot'>
366
+ /** Style resets **/
367
+ body {
368
+ font-family: arial, Helvetica, sans-serif;
369
+ margin: 0;
370
+ height: 100%;
371
+ overflow: hidden;
372
+ }
373
+ p {
374
+ margin: 0;
375
+ }
376
+ </style>
377
+ </head>
378
+ <body id='body-wvroot'>
379
+ <div id='wrapper-wvroot'></div>
380
+ </body>
381
+ </html>
382
+ HTML
383
+
384
+ CGI.escape(html)
385
+ end
386
+
387
+ public
388
+
389
+ # For now, the WebWrangler gets a bunch of fairly low-level requests
390
+ # to mess with the HTML DOM. This needs to be turned into a nicer API,
391
+ # but first we'll get it all into one place and see what we're doing.
392
+
393
+ # Replace the entire DOM - return a promise for when this has been done.
394
+ # This will often get rid of smaller changes in the queue, which is
395
+ # a good thing since they won't have to be run.
396
+ def replace(html_text)
397
+ @dom_wrangler.request_replace(html_text)
398
+ end
399
+
400
+ # Request a DOM change - return a promise for when this has been done.
401
+ def dom_change(js)
402
+ @dom_wrangler.request_change(js)
403
+ end
404
+
405
+ # Return whether the DOM is, right this moment, confirmed to be fully
406
+ # up to date or not.
407
+ # def dom_fully_updated?
408
+ # @dom_wrangler.fully_updated?
409
+ # end
410
+
411
+ # Return a promise that will be fulfilled when all current DOM changes
412
+ # have committed (but not necessarily any future DOM changes.)
413
+ def dom_redraw
414
+ @dom_wrangler.redraw
415
+ end
416
+
417
+ # Return a promise which will be fulfilled the next time the DOM is
418
+ # fully up to date. Note that a slow trickle of changes can make this
419
+ # take a long time, since it is *not* only changes up to this point.
420
+ # If you want to know that some specific change is done, it's often
421
+ # easiest to use the promise returned by dom_change(), which will
422
+ # be fulfilled when that specific change commits.
423
+ def dom_fully_updated
424
+ @dom_wrangler.fully_updated
425
+ end
426
+
427
+ def on_every_redraw(&block)
428
+ @dom_wrangler.on_every_redraw(&block)
429
+ end
430
+ end
431
+ end
432
+
433
+ # Leaving DOM changes as "meh, async, we'll see when it happens" is terrible for testing.
434
+ # Instead, we need to track whether particular changes have committed yet or not.
435
+ # So we add a single gateway for all DOM changes, and we make sure its work is done
436
+ # before we consider a redraw complete.
437
+ #
438
+ # DOMWrangler batches up changes - it's fine to have a redraw "in flight" and have
439
+ # changes waiting to catch the next bus. But we don't want more than one in flight,
440
+ # since it seems like having too many pending RPC requests can crash wasm. So:
441
+ # one redraw scheduled and one redraw promise waiting around, at maximum.
442
+ class Scarpe
443
+ class WebWrangler
444
+ class DOMWrangler
445
+ include Scarpe::Log
446
+
447
+ attr_reader :waiting_changes
448
+
449
+ # attr_reader :pending_redraw_promise
450
+ # attr_reader :waiting_redraw_promise
451
+
452
+ def initialize(web_wrangler, debug: false)
453
+ log_init("WASM::WebWrangler::DOMWrangler")
454
+
455
+ @wrangler = web_wrangler
456
+
457
+ @waiting_changes = []
458
+ # @pending_redraw_promise = nil
459
+ # @waiting_redraw_promise = nil
460
+
461
+ # @fully_up_to_date_promise = nil
462
+
463
+ # Initially we're waiting for a full replacement to happen.
464
+ # It's possible to request updates/changes before we have
465
+ # a DOM in place and before wasm is running. If we do
466
+ # that, we should discard those updates.
467
+ @first_draw_requested = false
468
+
469
+ @redraw_handlers = []
470
+
471
+ # The "fully up to date" logic is complicated and not
472
+ # as well tested as I'd like. This makes it far less
473
+ # likely that the event simply won't fire.
474
+ # With more comprehensive testing, this should be
475
+ # removable.
476
+ # web_wrangler.periodic_code("scarpeDOMWranglerHeartbeat") do
477
+ # if @fully_up_to_date_promise && fully_updated?
478
+ # @log.info("Fulfilling up-to-date promise on heartbeat")
479
+ # @fully_up_to_date_promise.fulfilled!
480
+ # @fully_up_to_date_promise = nil
481
+ # end
482
+ # end
483
+ end
484
+
485
+ def request_change(js_code)
486
+ @log.debug("Requesting change with code #{js_code}")
487
+ # No updates until there's something to update
488
+ return unless @first_draw_requested
489
+
490
+ @waiting_changes << js_code
491
+
492
+ redraw
493
+ end
494
+
495
+ def self.replacement_code(html_text)
496
+ "document.getElementById('wrapper-wvroot').innerHTML = `#{html_text}`; true"
497
+ end
498
+
499
+ def request_replace(html_text)
500
+ @log.debug("Entering request_replace")
501
+ # Replace other pending changes, they're not needed any more
502
+ @waiting_changes = [DOMWrangler.replacement_code(html_text)]
503
+ @first_draw_requested = true
504
+
505
+ @log.debug("Requesting DOM replacement...")
506
+ redraw
507
+ end
508
+
509
+ def on_every_redraw(&block)
510
+ @redraw_handlers << block
511
+ end
512
+
513
+ # What are the states of redraw?
514
+ # "empty" - no waiting promise, no pending-redraw promise, no pending changes
515
+ # "pending only" - no waiting promise, but we have a pending redraw with some changes; it hasn't committed yet
516
+ # "pending and waiting" - we have a waiting promise for our unscheduled changes; we can add more unscheduled
517
+ # changes since we haven't scheduled them yet.
518
+ #
519
+ # This is often called after adding a new waiting change or replacing them, so the state may have just changed.
520
+ # It can also be called when no changes have been made and no updates need to happen.
521
+ def redraw
522
+ # if fully_updated?
523
+ # # No changes to make, nothing in-process or waiting, so just return a pre-fulfilled promise
524
+ # @log.debug("Requesting redraw but there are no pending changes or promises, return pre-fulfilled")
525
+ # return Promise.fulfilled
526
+ # end
527
+
528
+ # Already have a redraw requested *and* one on deck? Then all current changes will have committed
529
+ # when we (eventually) fulfill the waiting_redraw_promise.
530
+ # if @waiting_redraw_promise
531
+ # @log.debug("Promising eventual redraw of #{@waiting_changes.size} waiting unscheduled changes.")
532
+ # return @waiting_redraw_promise
533
+ # end
534
+
535
+ # if @waiting_changes.empty?
536
+ # # There's no waiting_redraw_promise. There are no waiting changes. But we're not fully updated.
537
+ # # So there must be a redraw in flight, and we don't need to schedule a new waiting_redraw_promise.
538
+ # @log.debug("Returning in-flight redraw promise")
539
+ # return @pending_redraw_promise
540
+ # end
541
+
542
+ # @log.debug("Requesting redraw with #{@waiting_changes.size} waiting changes - need to schedule something!")
543
+
544
+ # We have at least one waiting change, possibly newly-added. We have no waiting_redraw_promise.
545
+ # Do we already have a redraw in-flight?
546
+ # if @pending_redraw_promise
547
+ # # Yes we do. Schedule a new waiting promise. When it turns into the pending_redraw_promise it will
548
+ # # grab all waiting changes. In the mean time, it sits here and waits.
549
+ # #
550
+ # # We *could* do a fancy promise thing and have it update @waiting_changes for itself, etc, when it
551
+ # # schedules itself. But we should always be calling promise_redraw or having a redraw fulfilled (see below)
552
+ # # when these things change. I'd rather keep the logic in this method. It's easier to reason through
553
+ # # all the cases.
554
+ # @waiting_redraw_promise = Promise.new
555
+
556
+ # @log.debug("Creating a new waiting promise since a pending promise is already in place")
557
+ # return @waiting_redraw_promise
558
+ # end
559
+
560
+ # We have no redraw in-flight and no pre-existing waiting line. The new change(s) are presumably right
561
+ # after things were fully up-to-date. We can schedule them for immediate redraw.
562
+
563
+ @log.debug("Requesting redraw with #{@waiting_changes.size} waiting changes - scheduling a new redraw for them!")
564
+ # promise = schedule_waiting_changes # This clears the waiting changes
565
+ schedule_waiting_changes
566
+ # @pending_redraw_promise = promise
567
+
568
+ @redraw_handlers.each(&:call)
569
+ # @pending_redraw_promise = nil
570
+
571
+ # if @waiting_redraw_promise
572
+ # # While this redraw was in flight, more waiting changes got added and we made a promise
573
+ # # about when they'd complete. Now they get scheduled, and we'll fulfill the waiting
574
+ # # promise when that redraw finishes. Clear the old waiting promise. We'll add a new one
575
+ # # when/if more changes are scheduled during this redraw.
576
+ # old_waiting_promise = @waiting_redraw_promise
577
+ # @waiting_redraw_promise = nil
578
+
579
+ # @log.debug "Fulfilled redraw with #{@waiting_changes.size} waiting changes - scheduling a new redraw for them!"
580
+
581
+ # new_promise = promise_redraw
582
+ # new_promise.on_fulfilled { old_waiting_promise.fulfilled! }
583
+ # else
584
+ # The in-flight redraw completed, and there's still no waiting promise. Good! That means
585
+ # we should be fully up-to-date.
586
+ # @log.debug "Fulfilled redraw with no waiting changes - marking us as up to date!"
587
+ # if @waiting_changes.empty?
588
+ # # We're fully up to date! Fulfill the promise. Now we don't need it again until somebody asks
589
+ # # us for another.
590
+ # if @fully_up_to_date_promise
591
+ # @fully_up_to_date_promise.fulfilled!
592
+ # @fully_up_to_date_promise = nil
593
+ # end
594
+ # else
595
+ # @log.error "WHOAH, WHAT? My logic must be wrong, because there's " +
596
+ # "no waiting promise, but waiting changes!"
597
+ # end
598
+ # end
599
+
600
+ # @log.debug("Redraw is now fully up-to-date") if fully_updated?
601
+ # end.on_rejected do
602
+ # @log.error "Could not complete JS redraw! #{promise.reason.full_message}"
603
+ # @log.debug("REDRAW FULLY UP TO DATE BUT JS FAILED") if fully_updated?
604
+
605
+ # raise "JS Redraw failed! Bailing!"
606
+
607
+ # # Later we should figure out how to handle this. Clear the promises and queues and request another redraw?
608
+ # end
609
+ end
610
+
611
+ # def fully_updated?
612
+ # @pending_redraw_promise.nil? && @waiting_redraw_promise.nil? && @waiting_changes.empty?
613
+ # end
614
+
615
+ # Return a promise which will be fulfilled when the DOM is fully up-to-date
616
+ # def promise_fully_updated
617
+ # if fully_updated?
618
+ # # No changes to make, nothing in-process or waiting, so just return a pre-fulfilled promise
619
+ # return Promise.fulfilled
620
+ # end
621
+
622
+ # # Do we already have a promise for this? Return it. Everybody can share one.
623
+ # if @fully_up_to_date_promise
624
+ # return @fully_up_to_date_promise
625
+ # end
626
+
627
+ # # We're not fully updated, so we need a promise. Create it, return it.
628
+ # @fully_up_to_date_promise = Promise.new
629
+ # end
630
+
631
+ private
632
+
633
+ # Put together the waiting changes into a new in-flight redraw request.
634
+ # Return it as a promise.
635
+ def schedule_waiting_changes
636
+ # return if @waiting_changes.empty?
637
+
638
+ js_code = @waiting_changes.join(";")
639
+ @waiting_changes = [] # They're not waiting any more!
640
+ @wrangler.eval_js_async(js_code)
641
+ end
642
+ end
643
+ end
644
+ end
645
+
646
+ # For now we don't need one of these to add DOM elements, just to manipulate them
647
+ # after initial render.
648
+ class Scarpe
649
+ class WebWrangler
650
+ class ElementWrangler
651
+ attr_reader :html_id
652
+
653
+ def initialize(html_id)
654
+ @webwrangler = WASMDisplayService.instance.wrangler
655
+ @html_id = html_id
656
+ end
657
+
658
+ def update
659
+ @webwrangler.dom_redraw
660
+ end
661
+
662
+ def value=(new_value)
663
+ @webwrangler.dom_change("document.getElementById('" + html_id + "').value = `" + new_value + "`; true")
664
+ end
665
+
666
+ def inner_text=(new_text)
667
+ @webwrangler.dom_change("document.getElementById('" + html_id + "').innerText = '" + new_text + "'; true")
668
+ end
669
+
670
+ def inner_html=(new_html)
671
+ @webwrangler.dom_change("document.getElementById(\"" + html_id + "\").innerHTML = `" + new_html + "`; true")
672
+ end
673
+
674
+ def remove
675
+ @webwrangler.dom_change("document.getElementById('" + html_id + "').remove(); true")
676
+ end
677
+ end
678
+ end
679
+ end