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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +54 -0
- data/README.md +2 -2
- data/Rakefile +23 -15
- data/demo/test_chartjs.html +17 -17
- data/demo/test_component.html +15 -15
- data/demo/test_error_boundary.html +41 -41
- data/demo/test_router.html +57 -57
- data/demo/tic-tac-toe.html +29 -29
- data/docs/architecture.md +43 -30
- data/lib/funicular/compiler.rb +13 -13
- data/lib/funicular/helpers/picoruby_helper.rb +24 -1
- data/lib/funicular/ssr/runtime.rb +2 -0
- data/lib/funicular/ssr.rb +2 -1
- data/lib/funicular/testing/node_runner.rb +1 -1
- data/lib/funicular/vendor/mrbc/VERSION +1 -0
- data/lib/funicular/vendor/{picorbc/picorbc.js → mrbc/mrbc.js} +14 -1
- data/lib/funicular/vendor/mrbc/mrbc.wasm +0 -0
- data/lib/funicular/vendor/picoruby/VERSION +1 -1
- data/lib/funicular/vendor/picoruby/debug/init.iife.js +19 -4
- data/lib/funicular/vendor/picoruby/debug/picoruby.js +16 -10
- data/lib/funicular/vendor/picoruby/debug/picoruby.wasm +0 -0
- data/lib/funicular/vendor/picoruby/dist/init.iife.js +19 -4
- data/lib/funicular/vendor/picoruby/dist/picoruby.js +1 -1
- data/lib/funicular/vendor/picoruby/dist/picoruby.wasm +0 -0
- data/lib/funicular/vendor/picoruby-test-node/VERSION +1 -1
- data/lib/funicular/vendor/picoruby-test-node/picoruby.js +85 -84
- data/lib/funicular/vendor/picoruby-test-node/picoruby.wasm +0 -0
- data/lib/funicular/vendor/picoruby-test-node/picoruby.wasm.map +1 -1
- data/lib/funicular/version.rb +1 -1
- data/lib/generators/funicular/chat/templates/funicular_chat_component.rb.tt +47 -46
- data/lib/tasks/funicular.rake +1 -1
- data/minitest/commands_routes_test.rb +97 -0
- data/minitest/compiler_test.rb +195 -0
- data/minitest/configuration_test.rb +64 -0
- data/minitest/fixtures/funicular_app/components/greeting_component.rb +6 -6
- data/minitest/form_for_test.rb +2 -2
- data/minitest/funicular_test.rb +28 -2
- data/minitest/hydration_test.rb +2 -2
- data/minitest/middleware_test.rb +154 -0
- data/minitest/picoruby_helper_test.rb +139 -0
- data/minitest/route_parser_test.rb +139 -0
- data/minitest/schema_test.rb +23 -0
- data/minitest/ssr_test.rb +57 -0
- data/minitest/support/rails_stub.rb +59 -0
- data/minitest/test_helper.rb +20 -0
- data/minitest/testing_test.rb +267 -0
- data/minitest/view_context_test.rb +101 -0
- data/mrblib/component.rb +158 -237
- data/mrblib/debug.rb +7 -6
- data/mrblib/differ.rb +3 -1
- data/mrblib/error_boundary.rb +19 -27
- data/mrblib/form_builder.rb +28 -24
- data/mrblib/funicular.rb +2 -1
- data/mrblib/html_serializer.rb +14 -13
- data/mrblib/http.rb +12 -1
- data/mrblib/patcher.rb +12 -9
- data/mrblib/router.rb +9 -5
- data/mrblib/runtime.rb +28 -0
- data/mrblib/styles.rb +13 -17
- data/mrblib/vdom.rb +90 -9
- data/mrblib/view_context.rb +135 -0
- data/sig/component.rbs +34 -85
- data/sig/error_boundary.rbs +4 -4
- data/sig/form_builder.rbs +2 -1
- data/sig/html_serializer.rbs +2 -1
- data/sig/http.rbs +1 -0
- data/sig/patcher.rbs +1 -1
- data/sig/router.rbs +2 -13
- data/sig/runtime.rbs +13 -0
- data/sig/styles.rbs +3 -4
- data/sig/vdom.rbs +11 -2
- data/sig/view_context.rbs +47 -0
- metadata +18 -5
- data/lib/funicular/vendor/picorbc/VERSION +0 -1
- data/lib/funicular/vendor/picorbc/picorbc.wasm +0 -0
data/demo/tic-tac-toe.html
CHANGED
|
@@ -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
|
|
100
|
-
current_move = state
|
|
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
|
|
120
|
-
current_move = state
|
|
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)
|
|
30
|
-
|
|
31
|
-
| `funicular.rb`
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
34
|
-
| `
|
|
35
|
-
| `
|
|
36
|
-
| `
|
|
37
|
-
| `
|
|
38
|
-
| `
|
|
39
|
-
| `
|
|
40
|
-
| `
|
|
41
|
-
| `
|
|
42
|
-
| `
|
|
43
|
-
| `
|
|
44
|
-
| `
|
|
45
|
-
| `
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
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
|
|
52
|
-
with `Patcher`. Event handlers are native DOM listeners,
|
|
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 `
|
|
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
|
-
`
|
|
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/
|
|
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
|
|
91
|
-
with `HTMLSerializer`. The state is also
|
|
92
|
-
so the browser can hydrate the markup
|
|
93
|
-
deterministic and free of browser-only
|
|
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
|
|
data/lib/funicular/compiler.rb
CHANGED
|
@@ -5,11 +5,11 @@ require "shellwords"
|
|
|
5
5
|
module Funicular
|
|
6
6
|
class Compiler
|
|
7
7
|
class NodeNotFoundError < StandardError; end
|
|
8
|
-
class
|
|
8
|
+
class MrbcMissingError < StandardError; end
|
|
9
9
|
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
|
47
|
-
unless File.exist?(
|
|
48
|
-
raise
|
|
49
|
-
Vendored
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
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('
|
|
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.
|
|
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
|
|
66
|
-
if (
|
|
67
|
-
console.error('
|
|
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
|
-
|
|
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
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
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
|
|
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
|
-
|
|
5952
|
+
serial_capture_copy,
|
|
5947
5953
|
/** @export */
|
|
5948
5954
|
serial_capture_start,
|
|
5949
5955
|
/** @export */
|
|
Binary file
|
|
@@ -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
|
|
66
|
-
if (
|
|
67
|
-
console.error('
|
|
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
|
-
|
|
84
|
+
const delay = progressed || gcSchedulerPending() === 1 ? 0 : IDLE_DELAY;
|
|
85
|
+
setTimeout(run, delay);
|
|
71
86
|
}
|
|
72
87
|
run();
|
|
73
88
|
};
|