funicular 0.2.1 → 0.3.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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -0
  3. data/README.md +2 -2
  4. data/Rakefile +23 -15
  5. data/demo/test_chartjs.html +17 -17
  6. data/demo/test_component.html +15 -15
  7. data/demo/test_error_boundary.html +41 -41
  8. data/demo/test_router.html +57 -57
  9. data/demo/tic-tac-toe.html +29 -29
  10. data/docs/architecture.md +43 -30
  11. data/lib/funicular/compiler.rb +13 -13
  12. data/lib/funicular/helpers/picoruby_helper.rb +24 -1
  13. data/lib/funicular/ssr/runtime.rb +2 -0
  14. data/lib/funicular/ssr.rb +2 -1
  15. data/lib/funicular/testing/node_runner.rb +1 -1
  16. data/lib/funicular/vendor/mrbc/VERSION +1 -0
  17. data/lib/funicular/vendor/{picorbc/picorbc.js → mrbc/mrbc.js} +14 -1
  18. data/lib/funicular/vendor/mrbc/mrbc.wasm +0 -0
  19. data/lib/funicular/vendor/picoruby/VERSION +1 -1
  20. data/lib/funicular/vendor/picoruby/debug/init.iife.js +19 -4
  21. data/lib/funicular/vendor/picoruby/debug/picoruby.js +16 -10
  22. data/lib/funicular/vendor/picoruby/debug/picoruby.wasm +0 -0
  23. data/lib/funicular/vendor/picoruby/dist/init.iife.js +19 -4
  24. data/lib/funicular/vendor/picoruby/dist/picoruby.js +1 -1
  25. data/lib/funicular/vendor/picoruby/dist/picoruby.wasm +0 -0
  26. data/lib/funicular/vendor/picoruby-test-node/VERSION +1 -1
  27. data/lib/funicular/vendor/picoruby-test-node/picoruby.js +85 -84
  28. data/lib/funicular/vendor/picoruby-test-node/picoruby.wasm +0 -0
  29. data/lib/funicular/vendor/picoruby-test-node/picoruby.wasm.map +1 -1
  30. data/lib/funicular/version.rb +1 -1
  31. data/lib/generators/funicular/chat/templates/funicular_chat_component.rb.tt +47 -46
  32. data/lib/tasks/funicular.rake +1 -1
  33. data/minitest/commands_routes_test.rb +97 -0
  34. data/minitest/compiler_test.rb +195 -0
  35. data/minitest/configuration_test.rb +64 -0
  36. data/minitest/fixtures/funicular_app/components/greeting_component.rb +6 -6
  37. data/minitest/form_for_test.rb +2 -2
  38. data/minitest/funicular_test.rb +28 -2
  39. data/minitest/hydration_test.rb +2 -2
  40. data/minitest/middleware_test.rb +154 -0
  41. data/minitest/picoruby_helper_test.rb +139 -0
  42. data/minitest/route_parser_test.rb +139 -0
  43. data/minitest/schema_test.rb +23 -0
  44. data/minitest/ssr_test.rb +57 -0
  45. data/minitest/support/rails_stub.rb +59 -0
  46. data/minitest/test_helper.rb +20 -0
  47. data/minitest/testing_test.rb +267 -0
  48. data/minitest/view_context_test.rb +101 -0
  49. data/mrblib/component.rb +158 -237
  50. data/mrblib/debug.rb +7 -6
  51. data/mrblib/differ.rb +3 -1
  52. data/mrblib/error_boundary.rb +19 -27
  53. data/mrblib/form_builder.rb +28 -24
  54. data/mrblib/funicular.rb +2 -1
  55. data/mrblib/html_serializer.rb +14 -13
  56. data/mrblib/http.rb +12 -1
  57. data/mrblib/patcher.rb +12 -9
  58. data/mrblib/router.rb +9 -5
  59. data/mrblib/runtime.rb +28 -0
  60. data/mrblib/styles.rb +13 -17
  61. data/mrblib/vdom.rb +90 -9
  62. data/mrblib/view_context.rb +135 -0
  63. data/sig/component.rbs +34 -85
  64. data/sig/error_boundary.rbs +4 -4
  65. data/sig/form_builder.rbs +2 -1
  66. data/sig/html_serializer.rbs +2 -1
  67. data/sig/http.rbs +1 -0
  68. data/sig/patcher.rbs +1 -1
  69. data/sig/router.rbs +2 -13
  70. data/sig/runtime.rbs +13 -0
  71. data/sig/styles.rbs +3 -4
  72. data/sig/vdom.rbs +11 -2
  73. data/sig/view_context.rbs +47 -0
  74. metadata +18 -5
  75. data/lib/funicular/vendor/picorbc/VERSION +0 -1
  76. data/lib/funicular/vendor/picorbc/picorbc.wasm +0 -0
@@ -38,8 +38,8 @@
38
38
  on_click.call if on_click
39
39
  end
40
40
 
41
- def render
42
- button(class: 'square', onclick: :handle_click) do
41
+ def render(h)
42
+ h.button(class: 'square', onclick: :handle_click) do
43
43
  props[:value] || ''
44
44
  end
45
45
  end
@@ -55,7 +55,7 @@
55
55
  }
56
56
  end
57
57
 
58
- def render
58
+ def render(h)
59
59
  squares = props[:squares]
60
60
  x_is_next = props[:x_is_next]
61
61
 
@@ -66,22 +66,22 @@
66
66
  "Next player: #{x_is_next ? 'X' : 'O'}"
67
67
  end
68
68
 
69
- div do
70
- div(class: 'status') { status }
71
- div(class: 'board-row') do
72
- component(Square, value: squares[0], on_click: handle_click(0))
73
- component(Square, value: squares[1], on_click: handle_click(1))
74
- component(Square, value: squares[2], on_click: handle_click(2))
69
+ h.div do |hh|
70
+ hh.div(class: 'status') { status }
71
+ hh.div(class: 'board-row') do |hhh|
72
+ hhh.component(Square, value: squares[0], on_click: handle_click(0))
73
+ hhh.component(Square, value: squares[1], on_click: handle_click(1))
74
+ hhh.component(Square, value: squares[2], on_click: handle_click(2))
75
75
  end
76
- div(class: 'board-row') do
77
- component(Square, value: squares[3], on_click: handle_click(3))
78
- component(Square, value: squares[4], on_click: handle_click(4))
79
- component(Square, value: squares[5], on_click: handle_click(5))
76
+ hh.div(class: 'board-row') do |hhh|
77
+ hhh.component(Square, value: squares[3], on_click: handle_click(3))
78
+ hhh.component(Square, value: squares[4], on_click: handle_click(4))
79
+ hhh.component(Square, value: squares[5], on_click: handle_click(5))
80
80
  end
81
- div(class: 'board-row') do
82
- component(Square, value: squares[6], on_click: handle_click(6))
83
- component(Square, value: squares[7], on_click: handle_click(7))
84
- component(Square, value: squares[8], on_click: handle_click(8))
81
+ hh.div(class: 'board-row') do |hhh|
82
+ hhh.component(Square, value: squares[6], on_click: handle_click(6))
83
+ hhh.component(Square, value: squares[7], on_click: handle_click(7))
84
+ hhh.component(Square, value: squares[8], on_click: handle_click(8))
85
85
  end
86
86
  end
87
87
  end
@@ -96,8 +96,8 @@
96
96
  end
97
97
 
98
98
  def handle_play(i)
99
- history = state.history
100
- current_move = state.current_move
99
+ history = state[:history]
100
+ current_move = state[:current_move]
101
101
  current_squares = history[current_move].dup
102
102
 
103
103
  current_squares[i] = (current_move % 2 == 0) ? 'X' : 'O'
@@ -115,30 +115,30 @@
115
115
  }
116
116
  end
117
117
 
118
- def render
119
- history = state.history
120
- current_move = state.current_move
118
+ def render(h)
119
+ history = state[:history]
120
+ current_move = state[:current_move]
121
121
  x_is_next = (current_move % 2 == 0)
122
122
  current_squares = history[current_move]
123
123
 
124
- div(class: 'game') do
125
- div(class: 'game-board') do
126
- component(Board,
124
+ h.div(class: 'game') do |hh|
125
+ hh.div(class: 'game-board') do |hhh|
126
+ hhh.component(Board,
127
127
  x_is_next: x_is_next,
128
128
  squares: current_squares,
129
129
  on_play: ->(i) { handle_play(i) }
130
130
  )
131
131
  end
132
- div(class: 'game-info') do
133
- ol do
132
+ hh.div(class: 'game-info') do |hhh|
133
+ hhh.ol do |hhhh|
134
134
  history.each_with_index do |_squares, move|
135
135
  description = if move > 0
136
136
  "Go to move ##{move}"
137
137
  else
138
138
  'Go to game start'
139
139
  end
140
- li(key: move) do
141
- button(onclick: jump_to(move)) { description }
140
+ hhhh.li(key: move) do |item|
141
+ item.button(onclick: jump_to(move)) { description }
142
142
  end
143
143
  end
144
144
  end
data/docs/architecture.md CHANGED
@@ -26,34 +26,47 @@ stay free of browser-only calls on any server code path
26
26
 
27
27
  ## `mrblib/` runtime: responsibilities
28
28
 
29
- | File(s) | Responsibility |
30
- |--------------------------------------------------|---------------------------------------------------------------------------|
31
- | `funicular.rb` | Top-level module: `start`, `router`, `server?`, `debug_color` export |
32
- | `component.rb` | `Funicular::Component` base: state, props, lifecycle, suspense, refs, styles |
33
- | `vdom.rb` | Virtual DOM nodes and the element-factory DSL (`div`, `button`, ...) |
34
- | `differ.rb` | `Differ.diff(old, new)` -- minimal patch set, key-based list reconciliation |
35
- | `patcher.rb` | `Patcher.apply(dom, patches)` -- apply patches to the real DOM |
36
- | `html_serializer.rb` | `VDOM::HTMLSerializer` -- VDOM to HTML string (used by SSR) |
37
- | `router.rb` | Client-side router, route DSL, `RouteHelpers` generation, History API |
38
- | `model.rb` | Object-REST Mapper (`all`/`find`/`create`/`update`/`destroy`) |
39
- | `http.rb` | Low-level fetch wrapper, CSRF, IndexedDB response cache |
40
- | `cable.rb` | ActionCable-compatible consumer/subscription client |
41
- | `store.rb`, `store_singleton.rb`, `store_collection.rb` | IndexedDB-backed stores, scope API, `subscribes_to`, event dispatch |
42
- | `form_builder.rb` | `form_for` and field helpers with inline error rendering |
43
- | `0_validations.rb`, `1_validators.rb` | ActiveModel-style validators and `errors` |
44
- | `styles.rb` | CSS-in-Ruby `styles` DSL and the `s` helper |
45
- | `error_boundary.rb` | `ErrorBoundary` component |
46
- | `file_upload.rb` | File / FormData upload helper |
47
- | `debug.rb` | Development-only component/error registry for the DevTools extension |
48
- | `environment_inquirer.rb` | Environment detection (`server?`, `development?`) |
29
+ | File(s) | Responsibility |
30
+ |---------------------------------------------------------|-------------------------------------------------------------------------------------------------|
31
+ | `funicular.rb` | Top-level module: `start`, `router`, `server?`, `debug_color` export |
32
+ | `runtime.rb` | Per-app runtime context propagated through render/SSR/hydration |
33
+ | `view_context.rb` | Thin render facade passed as `render(h)`: elements, components, forms, routes, styles, suspense |
34
+ | `component.rb` | `Funicular::Component` base: state, props, lifecycle, suspense loading, refs |
35
+ | `vdom.rb` | Virtual DOM nodes, including component vnodes with ordinary `children` |
36
+ | `differ.rb` | `Differ.diff(old, new)` -- minimal patch set, key-based list reconciliation |
37
+ | `patcher.rb` | `Patcher.apply(dom, patches)` -- apply patches to the real DOM |
38
+ | `html_serializer.rb` | `VDOM::HTMLSerializer` -- VDOM to HTML string (used by SSR) |
39
+ | `router.rb` | Client-side router, route DSL, per-runtime route helper object, History API |
40
+ | `model.rb` | Object-REST Mapper (`all`/`find`/`create`/`update`/`destroy`) |
41
+ | `http.rb` | Low-level fetch wrapper, CSRF, IndexedDB response cache |
42
+ | `cable.rb` | ActionCable-compatible consumer/subscription client |
43
+ | `store.rb`, `store_singleton.rb`, `store_collection.rb` | IndexedDB-backed stores, scope API, `subscribes_to`, event dispatch |
44
+ | `form_builder.rb` | `h.form_for` field helpers with inline error rendering |
45
+ | `0_validations.rb`, `1_validators.rb` | ActiveModel-style validators and `errors` |
46
+ | `styles.rb` | CSS-in-Ruby `styles { |css| css.define ... }` and `h.styles[...]` access |
47
+ | `error_boundary.rb` | `ErrorBoundary` component |
48
+ | `file_upload.rb` | File / FormData upload helper |
49
+ | `debug.rb` | Development-only component/error registry for the DevTools extension |
50
+ | `environment_inquirer.rb` | Environment detection (`server?`, `development?`) |
49
51
 
50
52
  The render cycle: a state change calls `patch()`, which rebuilds the component's
51
- VDOM, diffs it against the previous VDOM with `Differ`, and applies the result
52
- with `Patcher`. Event handlers are native DOM listeners, re-bound on each render.
53
+ VDOM by calling `render(h)`, diffs it against the previous VDOM with `Differ`,
54
+ and applies the result with `Patcher`. Event handlers are native DOM listeners,
55
+ re-bound on each render.
56
+
57
+ Application code receives all render helpers through `h`. HTML is authored as
58
+ `h.div`, custom elements as `h.tag(:custom_element)`, child components as
59
+ `h.component`, forms as `h.form_for`, styles as `h.styles[:name]`, resources as
60
+ `h.resources[:name]`, and routes as `h.routes.user_path(id)`. Component state is
61
+ explicitly read with `state[:name]` or `state.fetch(:name)`.
62
+
63
+ Component children are ordinary VDOM children stored on
64
+ `VDOM::Component#children`; there is no delayed `children_block` prop. This keeps
65
+ SSR, diffing, ErrorBoundary rendering, and hydration on the same data model.
53
66
 
54
67
  ## `lib/` Rails integration
55
68
 
56
- - `compiler.rb` -- runs the vendored `picorbc` (WebAssembly, via Node.js) to
69
+ - `compiler.rb` -- runs the vendored `mrbc` (WebAssembly, via Node.js) to
57
70
  compile `app/funicular/**/*.rb` (models, then stores, then components, then
58
71
  initializers) into a single `app/assets/builds/app.mrb`. `-g` is added in
59
72
  development for debug symbols.
@@ -73,12 +86,12 @@ with `Patcher`. Event handlers are native DOM listeners, re-bound on each render
73
86
  ## Vendored artifacts
74
87
 
75
88
  `rake copy_wasm` (run by `rake build`) copies the PicoRuby.wasm runtime and the
76
- `picorbc` compiler from the sibling `mrbgems/picoruby-wasm/npm/` directory into
89
+ `mrbc` compiler from the sibling `mrbgems/picoruby-wasm/npm/` directory into
77
90
  `lib/funicular/vendor/`:
78
91
 
79
92
  - `vendor/picoruby/dist/` -- production runtime build
80
93
  - `vendor/picoruby/debug/` -- development runtime build (debug symbols)
81
- - `vendor/picorbc/` -- the mruby compiler (run through Node.js)
94
+ - `vendor/mrbc/` -- the mruby compiler (run through Node.js)
82
95
 
83
96
  Because `copy_wasm` reads sibling directories inside the picoruby repository, it
84
97
  only works from within that checkout -- see Development below.
@@ -87,11 +100,11 @@ only works from within that checkout -- see Development below.
87
100
 
88
101
  For SSR the `mrblib/` framework is loaded into the Rails process under CRuby.
89
102
  `Funicular::SSR.render(path:, state:)` resolves the path against the routes in
90
- `app/funicular/initializer.rb`, builds the component's VDOM, and serializes it
91
- with `HTMLSerializer`. The state is also embedded as `window.__FUNICULAR_STATE__`
92
- so the browser can hydrate the markup rather than rebuild it. Keep `render`
93
- deterministic and free of browser-only calls so the same code is safe on both
94
- sides.
103
+ `app/funicular/initializer.rb`, builds a `Runtime` around that router, builds the
104
+ component's VDOM, and serializes it with `HTMLSerializer`. The state is also
105
+ embedded as `window.__FUNICULAR_STATE__` so the browser can hydrate the markup
106
+ rather than rebuild it. Keep `render(h)` deterministic and free of browser-only
107
+ calls so the same code is safe on both sides.
95
108
 
96
109
  ## Development
97
110
 
@@ -5,11 +5,11 @@ require "shellwords"
5
5
  module Funicular
6
6
  class Compiler
7
7
  class NodeNotFoundError < StandardError; end
8
- class PicorbcMissingError < StandardError; end
8
+ class MrbcMissingError < StandardError; end
9
9
 
10
- # picorbc.js + picorbc.wasm bundled into the gem at build time by `rake copy_wasm`.
11
- PICORBC_DIR = File.expand_path("vendor/picorbc", __dir__)
12
- PICORBC_JS = File.join(PICORBC_DIR, "picorbc.js")
10
+ # mrbc.js + mrbc.wasm bundled into the gem at build time by `rake copy_wasm`.
11
+ MRBC_DIR = File.expand_path("vendor/mrbc", __dir__)
12
+ MRBC_JS = File.join(MRBC_DIR, "mrbc.js")
13
13
 
14
14
  # Ordered list of application source files under app/funicular/.
15
15
  # Order matters: models -> stores -> components -> initializer, so that
@@ -36,19 +36,19 @@ module Funicular
36
36
  end
37
37
 
38
38
  def compile
39
- check_picorbc_availability!
39
+ check_mrbc_availability!
40
40
  gather_source_files
41
41
  compile_to_mrb
42
42
  end
43
43
 
44
44
  private
45
45
 
46
- def check_picorbc_availability!
47
- unless File.exist?(PICORBC_JS)
48
- raise PicorbcMissingError, <<~ERROR
49
- Vendored picorbc not found at #{PICORBC_JS}.
46
+ def check_mrbc_availability!
47
+ unless File.exist?(MRBC_JS)
48
+ raise MrbcMissingError, <<~ERROR
49
+ Vendored mrbc not found at #{MRBC_JS}.
50
50
 
51
- The funicular gem ships picorbc.js + picorbc.wasm inside the gem
51
+ The funicular gem ships mrbc.js + mrbc.wasm inside the gem
52
52
  package. This file is missing, which likely means the gem was not
53
53
  installed correctly. Try reinstalling:
54
54
 
@@ -61,7 +61,7 @@ module Funicular
61
61
  raise NodeNotFoundError, <<~ERROR
62
62
  Node.js executable not found.
63
63
 
64
- Funicular compiles Ruby to .mrb using a WebAssembly build of picorbc
64
+ Funicular compiles Ruby to .mrb using a WebAssembly build of mrbc
65
65
  which is run via Node.js. Please install Node.js and ensure `node`
66
66
  is on your PATH (or set the NODE environment variable).
67
67
  ERROR
@@ -110,7 +110,7 @@ module Funicular
110
110
  def compile_to_mrb
111
111
  all_files = @source_files.dup
112
112
  all_files << @env_file if @env_file
113
- argv = [node_command, PICORBC_JS]
113
+ argv = [node_command, MRBC_JS]
114
114
  argv << "-g" if debug_mode
115
115
  argv += ["-o", output_file.to_s]
116
116
  argv += all_files.map(&:to_s)
@@ -128,7 +128,7 @@ module Funicular
128
128
  result = system(*argv)
129
129
 
130
130
  unless result
131
- raise "Failed to compile with picorbc. Command: #{Shellwords.join(argv)}"
131
+ raise "Failed to compile with mrbc. Command: #{Shellwords.join(argv)}"
132
132
  end
133
133
 
134
134
  log "Successfully compiled to #{output_file}"
@@ -11,6 +11,11 @@ module Funicular
11
11
  local_dist: "/picoruby/dist/init.iife.js"
12
12
  }.freeze
13
13
 
14
+ LOCAL_VARIANTS = {
15
+ local_debug: "debug",
16
+ local_dist: "dist"
17
+ }.freeze
18
+
14
19
  # Minimal CSS the gem ships for class names it emits itself (e.g.
15
20
  # FormBuilder error states). Read once; see assets/funicular.css.
16
21
  BASE_CSS_PATH = File.expand_path("../assets/funicular.css", __dir__)
@@ -101,12 +106,30 @@ module Funicular
101
106
  end
102
107
  format(CDN_URL_TEMPLATE, version: version)
103
108
  elsif (path = LOCAL_PATHS[source])
104
- path
109
+ local_picoruby_src(path, source)
105
110
  else
106
111
  raise ArgumentError,
107
112
  "Unknown picoruby source: #{source.inspect}. Expected one of #{Funicular::Configuration::SOURCES.inspect}"
108
113
  end
109
114
  end
115
+
116
+ def local_picoruby_src(path, source)
117
+ cache_key = local_picoruby_cache_key(source)
118
+ return path if cache_key.nil?
119
+
120
+ "#{path}?v=#{cache_key}"
121
+ end
122
+
123
+ def local_picoruby_cache_key(source)
124
+ variant = LOCAL_VARIANTS.fetch(source)
125
+ version = Funicular.vendored_wasm_version
126
+ wasm = File.join(Funicular::VENDOR_PICORUBY_DIR, variant, "picoruby.wasm")
127
+ mtime = File.mtime(wasm).to_i
128
+
129
+ [version || Funicular::VERSION, mtime].join("-")
130
+ rescue Errno::ENOENT
131
+ Funicular.vendored_wasm_version || Funicular::VERSION
132
+ end
110
133
  end
111
134
  end
112
135
  end
@@ -27,6 +27,8 @@ module Funicular
27
27
  differ
28
28
  patcher
29
29
  styles
30
+ runtime
31
+ view_context
30
32
  debug
31
33
  component
32
34
  error_boundary
data/lib/funicular/ssr.rb CHANGED
@@ -30,8 +30,9 @@ module Funicular
30
30
  return { html: "", state: {}, component: nil } unless component_class
31
31
 
32
32
  instance = component_class.new(symbolize_keys(params).merge(props))
33
+ instance.runtime = Funicular::Runtime.new(router)
33
34
  instance.seed_state(state)
34
- html = Funicular::VDOM::HTMLSerializer.serialize(instance.build_vdom)
35
+ html = Funicular::VDOM::HTMLSerializer.serialize(instance.build_vdom, instance.runtime)
35
36
 
36
37
  { html: html, state: state, component: component_class }
37
38
  end
@@ -117,9 +117,9 @@ module Funicular
117
117
 
118
118
  def default_runtime_dir
119
119
  ENV["FUNICULAR_TEST_PICORUBY_DIR"] ||
120
- existing_path(File.join(gem_root, "lib", "funicular", "vendor", "picoruby-test-node")) ||
121
120
  existing_path(File.expand_path("../picoruby/build/picoruby-wasm-test/bin", gem_root)) ||
122
121
  existing_path(File.expand_path("../../build/picoruby-wasm-test/bin", gem_root)) ||
122
+ existing_path(File.join(gem_root, "lib", "funicular", "vendor", "picoruby-test-node")) ||
123
123
  File.join(gem_root, "lib", "funicular", "vendor", "picoruby-test-node")
124
124
  end
125
125
 
@@ -0,0 +1 @@
1
+ 4.0.1
@@ -619,7 +619,7 @@ function createExportWrapper(name, nargs) {
619
619
  var wasmBinaryFile;
620
620
 
621
621
  function findWasmBinary() {
622
- return locateFile('picorbc.wasm');
622
+ return locateFile('mrbc.wasm');
623
623
  }
624
624
 
625
625
  function getBinarySync(file) {
@@ -5128,6 +5128,8 @@ var wasmImports = {
5128
5128
  /** @export */
5129
5129
  invoke_vi,
5130
5130
  /** @export */
5131
+ invoke_vii,
5132
+ /** @export */
5131
5133
  invoke_viii
5132
5134
  };
5133
5135
 
@@ -5164,6 +5166,17 @@ function invoke_viii(index,a1,a2,a3) {
5164
5166
  }
5165
5167
  }
5166
5168
 
5169
+ function invoke_vii(index,a1,a2) {
5170
+ var sp = stackSave();
5171
+ try {
5172
+ getWasmTableEntry(index)(a1,a2);
5173
+ } catch(e) {
5174
+ stackRestore(sp);
5175
+ if (e !== e+0) throw e;
5176
+ _setThrew(1, 0);
5177
+ }
5178
+ }
5179
+
5167
5180
  function invoke_vi(index,a1) {
5168
5181
  var sp = stackSave();
5169
5182
  try {
Binary file
@@ -1 +1 @@
1
- 4.0.0
1
+ 4.0.1
@@ -47,7 +47,15 @@
47
47
  Module.picorubyRun = function() {
48
48
  const MRB_TICK_UNIT = 4; // Must match the value in build_config/picoruby-wasm.rb
49
49
  const BATCH_DURATION = 16; // ~1 frame (16.67ms)
50
+ const IDLE_DELAY = 4;
50
51
  const MAX_CATCHUP_TICKS = 10; // Cap to avoid freeze when tab returns from background
52
+ const runStepStatus = Module._mrb_run_step_status || function() {
53
+ const result = Module._mrb_run_step();
54
+ return result < 0 ? -1 : 1;
55
+ };
56
+ const gcSchedulerPending = Module._mrb_gc_scheduler_pending_wasm || function() {
57
+ return 0;
58
+ };
51
59
  let lastTick = performance.now();
52
60
  function run() {
53
61
  const now = performance.now();
@@ -61,13 +69,20 @@
61
69
  lastTick = now;
62
70
  }
63
71
  const sliceStart = performance.now();
72
+ let progressed = false;
64
73
  while (performance.now() - sliceStart < BATCH_DURATION) {
65
- const result = Module._mrb_run_step();
66
- if (result < 0) {
67
- console.error('mrb_run_step returned', result, '- scheduler continues');
74
+ const status = runStepStatus();
75
+ if (status < 0) {
76
+ console.error('mrb_run_step_status returned', status, '- scheduler continues');
77
+ break;
68
78
  }
79
+ if (status === 0) {
80
+ break;
81
+ }
82
+ progressed = true;
69
83
  }
70
- setTimeout(run, 0);
84
+ const delay = progressed || gcSchedulerPending() === 1 ? 0 : IDLE_DELAY;
85
+ setTimeout(run, delay);
71
86
  }
72
87
  run();
73
88
  };
@@ -5411,14 +5411,14 @@ function checkIncomingModuleAPI() {
5411
5411
  ignoredModuleProp('loadSplitModule');
5412
5412
  }
5413
5413
  var ASM_CONSTS = {
5414
- 2639196: ($0) => { globalThis.picorubyRefs[$0] = null; },
5415
- 2639236: ($0) => { globalThis.picorubyRefs[$0] = true; },
5416
- 2639276: ($0) => { globalThis.picorubyRefs[$0] = false; },
5417
- 2639317: ($0, $1) => { globalThis.picorubyRefs[$0] = $1; },
5418
- 2639355: ($0, $1) => { globalThis.picorubyRefs[$0] = $1; },
5419
- 2639393: ($0, $1, $2) => { const str = UTF8ToString($1, $2); globalThis.picorubyRefs[$0] = str; },
5420
- 2639466: ($0, $1) => { const arr = globalThis.picorubyRefs[$0]; const elem = globalThis.picorubyRefs[$1]; arr.push(elem); delete globalThis.picorubyRefs[$1]; },
5421
- 2639605: ($0, $1, $2) => { const obj = globalThis.picorubyRefs[$0]; const key = UTF8ToString($1); const val = globalThis.picorubyRefs[$2]; obj[key] = val; delete globalThis.picorubyRefs[$2]; }
5414
+ 2652134: ($0) => { globalThis.picorubyRefs[$0] = null; },
5415
+ 2652174: ($0) => { globalThis.picorubyRefs[$0] = true; },
5416
+ 2652214: ($0) => { globalThis.picorubyRefs[$0] = false; },
5417
+ 2652255: ($0, $1) => { globalThis.picorubyRefs[$0] = $1; },
5418
+ 2652293: ($0, $1) => { globalThis.picorubyRefs[$0] = $1; },
5419
+ 2652331: ($0, $1, $2) => { const str = UTF8ToString($1, $2); globalThis.picorubyRefs[$0] = str; },
5420
+ 2652404: ($0, $1) => { const arr = globalThis.picorubyRefs[$0]; const elem = globalThis.picorubyRefs[$1]; arr.push(elem); delete globalThis.picorubyRefs[$1]; },
5421
+ 2652543: ($0, $1, $2) => { const obj = globalThis.picorubyRefs[$0]; const key = UTF8ToString($1); const val = globalThis.picorubyRefs[$2]; obj[key] = val; delete globalThis.picorubyRefs[$2]; }
5422
5422
  };
5423
5423
  function ble_dataview_length(ref_id) { try { const dv = globalThis.picorubyRefs[ref_id]; if (dv && dv.byteLength !== undefined) { return dv.byteLength; } return 0; } catch(e) { console.error('ble_dataview_length failed:', e); return 0; } }
5424
5424
  function ble_dataview_read(ref_id,out_buf,max_len) { try { const dv = globalThis.picorubyRefs[ref_id]; if (!dv) return 0; const len = Math.min(dv.byteLength, max_len); for (let i = 0; i < len; i++) { HEAPU8[out_buf + i] = dv.getUint8(i); } return len; } catch(e) { console.error('ble_dataview_read failed:', e); return 0; } }
@@ -5507,7 +5507,7 @@ function serial_capture_start(ref_id) { try { const port = globalThis.picorubyRe
5507
5507
  function serial_binary_capture_start(ref_id) { try { const port = globalThis.picorubyRefs[ref_id]; if (!port) return; if (!globalThis.picorubySerialBinCap) { const buffers = new WeakMap(); const active = new WeakSet(); const MAX_BYTES = 256 * 1024; globalThis.picorubySerialBinCap = { start(p) { buffers.set(p, { chunks: [], totalBytes: 0 }); active.add(p); }, isActive(p) { return active.has(p); }, append(p, value) { const buf = buffers.get(p); if (!buf) return; const copy = new Uint8Array(value); buf.chunks.push(copy); buf.totalBytes += copy.length; while (buf.totalBytes > MAX_BYTES && buf.chunks.length > 1) { const removed = buf.chunks.shift(); buf.totalBytes -= removed.length; } }, read(p, outPtr, maxBytes) { const buf = buffers.get(p); if (!buf || buf.totalBytes === 0) return 0; let written = 0; while (written < maxBytes && buf.chunks.length > 0) { const chunk = buf.chunks[0]; const needed = maxBytes - written; if (chunk.length <= needed) { HEAPU8.set(chunk, outPtr + written); written += chunk.length; buf.chunks.shift(); } else { HEAPU8.set(chunk.subarray(0, needed), outPtr + written); buf.chunks[0] = chunk.subarray(needed); written += needed; } } buf.totalBytes -= written; return written; }, stop(p) { active.delete(p); buffers.delete(p); }, }; } globalThis.picorubySerialBinCap.start(port); } catch (e) { console.error('serial_binary_capture_start failed:', e); } }
5508
5508
  function serial_binary_capture_read(ref_id,out_buf,max_bytes) { try { const port = globalThis.picorubyRefs[ref_id]; if (!port || !globalThis.picorubySerialBinCap) return 0; return globalThis.picorubySerialBinCap.read(port, out_buf, max_bytes); } catch (e) { console.error('serial_binary_capture_read failed:', e); return 0; } }
5509
5509
  function serial_binary_capture_stop(ref_id) { try { const port = globalThis.picorubyRefs[ref_id]; if (!port || !globalThis.picorubySerialBinCap) return; globalThis.picorubySerialBinCap.stop(port); } catch (e) { console.error('serial_binary_capture_stop failed:', e); } }
5510
- function serial_capture_get(ref_id,stop) { try { const port = globalThis.picorubyRefs[ref_id]; if (!port || !globalThis.picorubySerialCapture) { return globalThis.picorubyRefs.push("") - 1; } const cap = globalThis.picorubySerialCapture; const out = stop ? cap.stop(port) : cap.peek(port); return globalThis.picorubyRefs.push(out) - 1; } catch (e) { console.error('serial_capture_get failed:', e); return globalThis.picorubyRefs.push("") - 1; } }
5510
+ function serial_capture_copy(ref_id,stop,out_buf,max_bytes) { try { const port = globalThis.picorubyRefs[ref_id]; if (!port || !globalThis.picorubySerialCapture) { if (max_bytes > 0) HEAPU8[out_buf] = 0; return 0; } const cap = globalThis.picorubySerialCapture; const out = stop ? cap.stop(port) : cap.peek(port); const bytes = new TextEncoder().encode(out); const n = Math.min(bytes.length, Math.max(0, max_bytes - 1)); if (n > 0) { HEAPU8.set(bytes.subarray(0, n), out_buf); } if (max_bytes > 0) { HEAPU8[out_buf + n] = 0; } return n; } catch (e) { console.error('serial_capture_copy failed:', e); if (max_bytes > 0) HEAPU8[out_buf] = 0; return 0; } }
5511
5511
  function ws_new(url) { try { const ws = new WebSocket(UTF8ToString(url)); const refId = globalThis.picorubyRefs.push(ws) - 1; return refId; } catch(e) { console.error('WebSocket creation failed:', e); return -1; } }
5512
5512
  function ws_send(ref_id,data) { try { const ws = globalThis.picorubyRefs[ref_id]; if (ws && ws.readyState === WebSocket.OPEN) { ws.send(UTF8ToString(data)); } } catch(e) { console.error('WebSocket send failed:', e); } }
5513
5513
  function ws_send_binary(ref_id,data,length) { try { const ws = globalThis.picorubyRefs[ref_id]; if (ws && ws.readyState === WebSocket.OPEN) { const buffer = new Uint8Array(HEAPU8.buffer, data, length); const copy = new Uint8Array(buffer); ws.send(copy.buffer); } } catch(e) { console.error('WebSocket send_binary failed:', e); } }
@@ -5545,6 +5545,8 @@ var _resume_binary_task = Module['_resume_binary_task'] = makeInvalidEarlyAccess
5545
5545
  var _call_ruby_callback_sync_generic = Module['_call_ruby_callback_sync_generic'] = makeInvalidEarlyAccess('_call_ruby_callback_sync_generic');
5546
5546
  var _mrb_tick_wasm = Module['_mrb_tick_wasm'] = makeInvalidEarlyAccess('_mrb_tick_wasm');
5547
5547
  var _mrb_run_step = Module['_mrb_run_step'] = makeInvalidEarlyAccess('_mrb_run_step');
5548
+ var _mrb_run_step_status = Module['_mrb_run_step_status'] = makeInvalidEarlyAccess('_mrb_run_step_status');
5549
+ var _mrb_gc_scheduler_pending_wasm = Module['_mrb_gc_scheduler_pending_wasm'] = makeInvalidEarlyAccess('_mrb_gc_scheduler_pending_wasm');
5548
5550
  var _picorb_init = Module['_picorb_init'] = makeInvalidEarlyAccess('_picorb_init');
5549
5551
  var _picorb_create_task = Module['_picorb_create_task'] = makeInvalidEarlyAccess('_picorb_create_task');
5550
5552
  var _picorb_create_task_with_filename = Module['_picorb_create_task_with_filename'] = makeInvalidEarlyAccess('_picorb_create_task_with_filename');
@@ -5591,6 +5593,8 @@ function assignWasmExports(wasmExports) {
5591
5593
  assert(typeof wasmExports['call_ruby_callback_sync_generic'] != 'undefined', 'missing Wasm export: call_ruby_callback_sync_generic');
5592
5594
  assert(typeof wasmExports['mrb_tick_wasm'] != 'undefined', 'missing Wasm export: mrb_tick_wasm');
5593
5595
  assert(typeof wasmExports['mrb_run_step'] != 'undefined', 'missing Wasm export: mrb_run_step');
5596
+ assert(typeof wasmExports['mrb_run_step_status'] != 'undefined', 'missing Wasm export: mrb_run_step_status');
5597
+ assert(typeof wasmExports['mrb_gc_scheduler_pending_wasm'] != 'undefined', 'missing Wasm export: mrb_gc_scheduler_pending_wasm');
5594
5598
  assert(typeof wasmExports['picorb_init'] != 'undefined', 'missing Wasm export: picorb_init');
5595
5599
  assert(typeof wasmExports['picorb_create_task'] != 'undefined', 'missing Wasm export: picorb_create_task');
5596
5600
  assert(typeof wasmExports['picorb_create_task_with_filename'] != 'undefined', 'missing Wasm export: picorb_create_task_with_filename');
@@ -5633,6 +5637,8 @@ function assignWasmExports(wasmExports) {
5633
5637
  _call_ruby_callback_sync_generic = Module['_call_ruby_callback_sync_generic'] = createExportWrapper('call_ruby_callback_sync_generic', 3);
5634
5638
  _mrb_tick_wasm = Module['_mrb_tick_wasm'] = createExportWrapper('mrb_tick_wasm', 0);
5635
5639
  _mrb_run_step = Module['_mrb_run_step'] = createExportWrapper('mrb_run_step', 0);
5640
+ _mrb_run_step_status = Module['_mrb_run_step_status'] = createExportWrapper('mrb_run_step_status', 0);
5641
+ _mrb_gc_scheduler_pending_wasm = Module['_mrb_gc_scheduler_pending_wasm'] = createExportWrapper('mrb_gc_scheduler_pending_wasm', 0);
5636
5642
  _picorb_init = Module['_picorb_init'] = createExportWrapper('picorb_init', 0);
5637
5643
  _picorb_create_task = Module['_picorb_create_task'] = createExportWrapper('picorb_create_task', 1);
5638
5644
  _picorb_create_task_with_filename = Module['_picorb_create_task_with_filename'] = createExportWrapper('picorb_create_task_with_filename', 2);
@@ -5943,7 +5949,7 @@ var wasmImports = {
5943
5949
  /** @export */
5944
5950
  serial_binary_capture_stop,
5945
5951
  /** @export */
5946
- serial_capture_get,
5952
+ serial_capture_copy,
5947
5953
  /** @export */
5948
5954
  serial_capture_start,
5949
5955
  /** @export */
@@ -47,7 +47,15 @@
47
47
  Module.picorubyRun = function() {
48
48
  const MRB_TICK_UNIT = 4; // Must match the value in build_config/picoruby-wasm.rb
49
49
  const BATCH_DURATION = 16; // ~1 frame (16.67ms)
50
+ const IDLE_DELAY = 4;
50
51
  const MAX_CATCHUP_TICKS = 10; // Cap to avoid freeze when tab returns from background
52
+ const runStepStatus = Module._mrb_run_step_status || function() {
53
+ const result = Module._mrb_run_step();
54
+ return result < 0 ? -1 : 1;
55
+ };
56
+ const gcSchedulerPending = Module._mrb_gc_scheduler_pending_wasm || function() {
57
+ return 0;
58
+ };
51
59
  let lastTick = performance.now();
52
60
  function run() {
53
61
  const now = performance.now();
@@ -61,13 +69,20 @@
61
69
  lastTick = now;
62
70
  }
63
71
  const sliceStart = performance.now();
72
+ let progressed = false;
64
73
  while (performance.now() - sliceStart < BATCH_DURATION) {
65
- const result = Module._mrb_run_step();
66
- if (result < 0) {
67
- console.error('mrb_run_step returned', result, '- scheduler continues');
74
+ const status = runStepStatus();
75
+ if (status < 0) {
76
+ console.error('mrb_run_step_status returned', status, '- scheduler continues');
77
+ break;
68
78
  }
79
+ if (status === 0) {
80
+ break;
81
+ }
82
+ progressed = true;
69
83
  }
70
- setTimeout(run, 0);
84
+ const delay = progressed || gcSchedulerPending() === 1 ? 0 : IDLE_DELAY;
85
+ setTimeout(run, delay);
71
86
  }
72
87
  run();
73
88
  };