scarpe-wasm 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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