homura-runtime 0.3.5 → 0.3.7
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 +18 -0
- data/exe/auto-await +42 -27
- data/exe/compile-assets +46 -37
- data/exe/compile-erb +86 -61
- data/exe/homura-build +223 -119
- data/lib/homura/runtime/ai.rb +316 -22
- data/lib/homura/runtime/async_registry.rb +135 -98
- data/lib/homura/runtime/auto_await/analyzer.rb +34 -19
- data/lib/homura/runtime/auto_await/transformer.rb +1 -1
- data/lib/homura/runtime/build_support.rb +74 -38
- data/lib/homura/runtime/cache.rb +29 -22
- data/lib/homura/runtime/durable_object.rb +110 -56
- data/lib/homura/runtime/email.rb +28 -14
- data/lib/homura/runtime/http.rb +5 -4
- data/lib/homura/runtime/multipart.rb +47 -47
- data/lib/homura/runtime/queue.rb +82 -29
- data/lib/homura/runtime/scheduled.rb +29 -19
- data/lib/homura/runtime/stream.rb +30 -24
- data/lib/homura/runtime/version.rb +1 -1
- data/lib/homura/runtime.rb +330 -131
- data/lib/homura_vendor_tempfile.rb +5 -4
- data/lib/homura_vendor_tilt.rb +4 -3
- data/lib/homura_vendor_zlib.rb +20 -13
- data/lib/opal_patches.rb +196 -109
- metadata +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require "set"
|
|
4
4
|
|
|
5
5
|
module HomuraRuntime
|
|
6
6
|
class AsyncRegistry
|
|
@@ -22,11 +22,15 @@ module HomuraRuntime
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def taint_return(class_name, method_name, return_class_name)
|
|
25
|
-
(@registry.taint_returns[class_name] ||= {})[
|
|
25
|
+
(@registry.taint_returns[class_name] ||= {})[
|
|
26
|
+
method_name
|
|
27
|
+
] = return_class_name
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
def async_accessor(lvar_name, accessor_name, class_name)
|
|
29
|
-
@registry.async_accessors[
|
|
31
|
+
@registry.async_accessors[
|
|
32
|
+
[lvar_name.to_sym, accessor_name.to_sym]
|
|
33
|
+
] = class_name
|
|
30
34
|
end
|
|
31
35
|
|
|
32
36
|
def async_helper(method_name, class_name)
|
|
@@ -75,28 +79,46 @@ module HomuraRuntime
|
|
|
75
79
|
Gem.loaded_specs.each_value do |spec|
|
|
76
80
|
next if spec.full_gem_path.nil?
|
|
77
81
|
|
|
78
|
-
lib_dir = File.join(spec.full_gem_path,
|
|
82
|
+
lib_dir = File.join(spec.full_gem_path, "lib")
|
|
79
83
|
next unless Dir.exist?(lib_dir)
|
|
80
84
|
|
|
81
|
-
Dir
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
85
|
+
Dir
|
|
86
|
+
.glob(File.join(lib_dir, "**", "*.rb"))
|
|
87
|
+
.each do |path|
|
|
88
|
+
next unless File.read(path).include?("register_async_source")
|
|
89
|
+
|
|
90
|
+
require_path =
|
|
91
|
+
path.sub(Regexp.new("^#{Regexp.escape(lib_dir)}/"), "").sub(
|
|
92
|
+
/\.rb\z/,
|
|
93
|
+
""
|
|
94
|
+
)
|
|
95
|
+
begin
|
|
96
|
+
require require_path
|
|
97
|
+
loaded += 1
|
|
98
|
+
if debug
|
|
99
|
+
puts "[auto-await] loaded async source from #{spec.name}: #{require_path}"
|
|
100
|
+
end
|
|
101
|
+
rescue LoadError, StandardError => e
|
|
102
|
+
if debug
|
|
103
|
+
warn "[auto-await] Warning: failed to load async source from #{spec.name}/#{require_path}: #{e.message}"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
91
106
|
end
|
|
92
|
-
end
|
|
93
107
|
end
|
|
94
108
|
|
|
95
|
-
|
|
109
|
+
if debug && loaded.positive?
|
|
110
|
+
puts "[auto-await] auto-loaded #{loaded} async source file(s)"
|
|
111
|
+
end
|
|
96
112
|
end
|
|
97
113
|
end
|
|
98
114
|
|
|
99
|
-
attr_reader :async_classes,
|
|
115
|
+
attr_reader :async_classes,
|
|
116
|
+
:async_methods,
|
|
117
|
+
:async_factories,
|
|
118
|
+
:taint_returns,
|
|
119
|
+
:async_accessors,
|
|
120
|
+
:async_helpers,
|
|
121
|
+
:helper_factories
|
|
100
122
|
|
|
101
123
|
def initialize
|
|
102
124
|
@async_classes = {}
|
|
@@ -113,7 +135,10 @@ module HomuraRuntime
|
|
|
113
135
|
methods = @async_methods[class_name]
|
|
114
136
|
return true if methods&.include?(method_name)
|
|
115
137
|
except = @async_classes[class_name]
|
|
116
|
-
|
|
138
|
+
if except && !except.include?(method_name.to_s) &&
|
|
139
|
+
!except.include?(method_name.to_sym)
|
|
140
|
+
return true
|
|
141
|
+
end
|
|
117
142
|
false
|
|
118
143
|
end
|
|
119
144
|
|
|
@@ -126,10 +151,8 @@ module HomuraRuntime
|
|
|
126
151
|
end
|
|
127
152
|
|
|
128
153
|
def tainted_class?(class_name)
|
|
129
|
-
@async_classes.key?(class_name) ||
|
|
130
|
-
@
|
|
131
|
-
@async_factories.key?(class_name) ||
|
|
132
|
-
@taint_returns.key?(class_name)
|
|
154
|
+
@async_classes.key?(class_name) || @async_methods.key?(class_name) ||
|
|
155
|
+
@async_factories.key?(class_name) || @taint_returns.key?(class_name)
|
|
133
156
|
end
|
|
134
157
|
end
|
|
135
158
|
end
|
|
@@ -138,86 +161,100 @@ end
|
|
|
138
161
|
# Each binding declares which methods return Promises so the
|
|
139
162
|
# build-time analyzer can insert .__await__ automatically.
|
|
140
163
|
HomuraRuntime::AsyncRegistry.register_async_source do
|
|
141
|
-
async_method
|
|
142
|
-
async_method
|
|
143
|
-
async_method
|
|
144
|
-
async_method
|
|
145
|
-
taint_return
|
|
146
|
-
taint_return
|
|
147
|
-
|
|
148
|
-
async_method
|
|
149
|
-
async_method
|
|
150
|
-
async_method
|
|
164
|
+
async_method "Cloudflare::D1Database", :execute
|
|
165
|
+
async_method "Cloudflare::D1Database", :get_first_row
|
|
166
|
+
async_method "Cloudflare::D1Database", :execute_insert
|
|
167
|
+
async_method "Cloudflare::D1Database", :execute_batch
|
|
168
|
+
taint_return "Cloudflare::D1Database", :prepare, "Cloudflare::D1Statement"
|
|
169
|
+
taint_return "Cloudflare::D1Database", :[], "Cloudflare::D1Statement"
|
|
170
|
+
|
|
171
|
+
async_method "Cloudflare::D1Statement", :all
|
|
172
|
+
async_method "Cloudflare::D1Statement", :first
|
|
173
|
+
async_method "Cloudflare::D1Statement", :run
|
|
151
174
|
# `bind` returns a new D1Statement for further chaining. Tainting the
|
|
152
175
|
# return preserves the type so the auto-await pass keeps chaining
|
|
153
176
|
# `.run` / `.all` / `.first` on the bound statement (otherwise
|
|
154
177
|
# `db.prepare(sql).bind(...).run` drops await on `.run` and
|
|
155
178
|
# `flatten_meta` ends up receiving a JS Promise instead of the
|
|
156
179
|
# metadata Hash).
|
|
157
|
-
taint_return
|
|
158
|
-
|
|
159
|
-
async_method
|
|
160
|
-
async_method
|
|
161
|
-
async_method
|
|
162
|
-
async_method
|
|
163
|
-
async_method
|
|
164
|
-
|
|
165
|
-
async_method
|
|
166
|
-
async_method
|
|
167
|
-
async_method
|
|
168
|
-
async_method
|
|
169
|
-
async_method
|
|
170
|
-
async_method
|
|
171
|
-
|
|
172
|
-
async_method
|
|
173
|
-
|
|
174
|
-
async_method
|
|
175
|
-
taint_return
|
|
176
|
-
|
|
177
|
-
async_method
|
|
178
|
-
async_method
|
|
179
|
-
async_method
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
async_method
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
async_method
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
async_method
|
|
193
|
-
async_method
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
async_method
|
|
207
|
-
async_method
|
|
208
|
-
async_method
|
|
209
|
-
async_method
|
|
210
|
-
async_method
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
180
|
+
taint_return "Cloudflare::D1Statement", :bind, "Cloudflare::D1Statement"
|
|
181
|
+
|
|
182
|
+
async_method "Cloudflare::KVNamespace", :get
|
|
183
|
+
async_method "Cloudflare::KVNamespace", :get_with_metadata
|
|
184
|
+
async_method "Cloudflare::KVNamespace", :put
|
|
185
|
+
async_method "Cloudflare::KVNamespace", :delete
|
|
186
|
+
async_method "Cloudflare::KVNamespace", :list
|
|
187
|
+
|
|
188
|
+
async_method "Cloudflare::R2Bucket", :get
|
|
189
|
+
async_method "Cloudflare::R2Bucket", :get_binary
|
|
190
|
+
async_method "Cloudflare::R2Bucket", :put
|
|
191
|
+
async_method "Cloudflare::R2Bucket", :delete
|
|
192
|
+
async_method "Cloudflare::R2Bucket", :list
|
|
193
|
+
async_method "Cloudflare::R2Bucket", :head
|
|
194
|
+
|
|
195
|
+
async_method "Cloudflare::AI", :run
|
|
196
|
+
async_method "Cloudflare::AI", :speak
|
|
197
|
+
async_method "Cloudflare::AI", :speak_data_url
|
|
198
|
+
taint_return "Cloudflare::AI", :run_stream, "Cloudflare::AI::Stream"
|
|
199
|
+
async_method "Cloudflare::AI::Binding", :run
|
|
200
|
+
async_method "Cloudflare::AI::Binding", :chat
|
|
201
|
+
async_method "Cloudflare::AI::Binding", :chat_text
|
|
202
|
+
async_method "Cloudflare::AI::Binding", :transcribe
|
|
203
|
+
async_method "Cloudflare::AI::Binding", :transcribe_text
|
|
204
|
+
async_method "Cloudflare::AI::Binding", :speak
|
|
205
|
+
async_method "Cloudflare::AI::Binding", :speak_data_url
|
|
206
|
+
taint_return "Cloudflare::AI::Binding", :run_stream, "Cloudflare::AI::Stream"
|
|
207
|
+
|
|
208
|
+
async_method "Cloudflare::Cache", :match
|
|
209
|
+
async_method "Cloudflare::Cache", :put
|
|
210
|
+
async_method "Cloudflare::Cache", :delete
|
|
211
|
+
|
|
212
|
+
async_factory "Cloudflare::Email", :new
|
|
213
|
+
async_method "Cloudflare::Email", :send
|
|
214
|
+
|
|
215
|
+
async_method "Cloudflare::Queue", :send
|
|
216
|
+
async_method "Cloudflare::Queue", :send_batch
|
|
217
|
+
|
|
218
|
+
async_factory "Cloudflare::DurableObjectNamespace", :new
|
|
219
|
+
taint_return "Cloudflare::DurableObjectNamespace",
|
|
220
|
+
:get,
|
|
221
|
+
"Cloudflare::DurableObjectStub"
|
|
222
|
+
taint_return "Cloudflare::DurableObjectNamespace",
|
|
223
|
+
:get_by_name,
|
|
224
|
+
"Cloudflare::DurableObjectStub"
|
|
225
|
+
taint_return "Cloudflare::DurableObjectState",
|
|
226
|
+
:storage,
|
|
227
|
+
"Cloudflare::DurableObjectStorage"
|
|
228
|
+
async_method "Cloudflare::DurableObjectStub", :fetch
|
|
229
|
+
async_method "Cloudflare::DurableObjectStub", :request
|
|
230
|
+
async_method "Cloudflare::DurableObjectStub", :get
|
|
231
|
+
async_method "Cloudflare::DurableObjectStub", :post
|
|
232
|
+
async_method "Cloudflare::DurableObjectStub", :put
|
|
233
|
+
async_method "Cloudflare::DurableObjectStub", :delete
|
|
234
|
+
|
|
235
|
+
async_method "Cloudflare::DurableObjectStorage", :get
|
|
236
|
+
async_method "Cloudflare::DurableObjectStorage", :put
|
|
237
|
+
async_method "Cloudflare::DurableObjectStorage", :delete
|
|
238
|
+
async_method "Cloudflare::DurableObjectStorage", :list
|
|
239
|
+
async_method "Cloudflare::DurableObjectStorage", :transaction
|
|
240
|
+
|
|
241
|
+
async_method "Cloudflare::HTTP", :fetch
|
|
242
|
+
|
|
243
|
+
async_method "Faraday::Connection", :get
|
|
244
|
+
async_method "Faraday::Connection", :post
|
|
245
|
+
async_method "Faraday::Connection", :put
|
|
246
|
+
async_method "Faraday::Connection", :delete
|
|
247
|
+
async_method "Faraday::Connection", :patch
|
|
248
|
+
async_method "Faraday::Connection", :head
|
|
249
|
+
|
|
250
|
+
helper_factory :d1, "Cloudflare::D1Database"
|
|
251
|
+
helper_factory :db, "Cloudflare::D1Database"
|
|
252
|
+
helper_factory :kv, "Cloudflare::KVNamespace"
|
|
253
|
+
helper_factory :bucket, "Cloudflare::R2Bucket"
|
|
254
|
+
helper_factory :ai, "Cloudflare::AI::Binding"
|
|
255
|
+
helper_factory :send_email, "Cloudflare::Email"
|
|
256
|
+
helper_factory :jobs_queue, "Cloudflare::Queue"
|
|
257
|
+
helper_factory :jobs_dlq, "Cloudflare::Queue"
|
|
258
|
+
helper_factory :do_counter, "Cloudflare::DurableObjectNamespace"
|
|
259
|
+
helper_factory :durable_object, "Cloudflare::DurableObjectStub"
|
|
223
260
|
end
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
3
|
+
require "parser/current"
|
|
4
|
+
require "parser/source/tree_rewriter"
|
|
5
|
+
require "set"
|
|
6
6
|
|
|
7
7
|
module HomuraRuntime
|
|
8
8
|
module AutoAwait
|
|
@@ -16,7 +16,7 @@ module HomuraRuntime
|
|
|
16
16
|
@async_local_methods = Set.new
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def process(source, filename =
|
|
19
|
+
def process(source, filename = "(auto-await)")
|
|
20
20
|
buffer = Parser::Source::Buffer.new(filename)
|
|
21
21
|
buffer.source = source
|
|
22
22
|
parser = Parser::CurrentRuby.new
|
|
@@ -52,7 +52,9 @@ module HomuraRuntime
|
|
|
52
52
|
# Bottom-up traversal: process children first so helper-factory
|
|
53
53
|
# sends (e.g. bare +db+) populate @env before their parent
|
|
54
54
|
# send (e.g. +db.execute(...)+) is checked by should_await?.
|
|
55
|
-
node.children.each
|
|
55
|
+
node.children.each do |child|
|
|
56
|
+
process_node(child) if child.is_a?(Parser::AST::Node)
|
|
57
|
+
end
|
|
56
58
|
case node.type
|
|
57
59
|
when :lvasgn
|
|
58
60
|
process_lvasgn(node)
|
|
@@ -85,13 +87,16 @@ module HomuraRuntime
|
|
|
85
87
|
before_awaits = @await_nodes.length
|
|
86
88
|
@env = @registry.helper_factories.dup
|
|
87
89
|
|
|
88
|
-
node.children[1..-1].each
|
|
90
|
+
node.children[1..-1].each do |child|
|
|
91
|
+
process_node(child) if child.is_a?(Parser::AST::Node)
|
|
92
|
+
end
|
|
89
93
|
|
|
90
94
|
body = node.children[2]
|
|
91
95
|
return_cls = infer_class(body)
|
|
92
96
|
@method_returns[method_name] = return_cls if return_cls
|
|
93
97
|
body_source = node.loc.expression&.source.to_s
|
|
94
|
-
if @await_nodes.length > before_awaits ||
|
|
98
|
+
if @await_nodes.length > before_awaits ||
|
|
99
|
+
body_source.include?(".__await__")
|
|
95
100
|
@async_local_methods << method_name
|
|
96
101
|
end
|
|
97
102
|
|
|
@@ -115,7 +120,8 @@ module HomuraRuntime
|
|
|
115
120
|
|
|
116
121
|
def process_send(node)
|
|
117
122
|
receiver, method_name = *node
|
|
118
|
-
if receiver.nil? &&
|
|
123
|
+
if receiver.nil? &&
|
|
124
|
+
(factory_cls = @registry.helper_factories[method_name])
|
|
119
125
|
@env[method_name] = factory_cls
|
|
120
126
|
end
|
|
121
127
|
if should_await?(node)
|
|
@@ -144,7 +150,9 @@ module HomuraRuntime
|
|
|
144
150
|
when :send
|
|
145
151
|
receiver, method_name = *node
|
|
146
152
|
if receiver.nil?
|
|
147
|
-
|
|
153
|
+
if @method_returns.key?(method_name)
|
|
154
|
+
return @method_returns[method_name]
|
|
155
|
+
end
|
|
148
156
|
return @env[method_name] if @env.key?(method_name)
|
|
149
157
|
end
|
|
150
158
|
infer_send_class(node)
|
|
@@ -175,7 +183,8 @@ module HomuraRuntime
|
|
|
175
183
|
key_node = node.children[2]
|
|
176
184
|
if key_node&.type == :str
|
|
177
185
|
key = key_node.children[0]
|
|
178
|
-
mapped =
|
|
186
|
+
mapped =
|
|
187
|
+
@registry.async_accessors[[env_name(receiver), key.to_sym]]
|
|
179
188
|
return mapped if mapped
|
|
180
189
|
end
|
|
181
190
|
end
|
|
@@ -189,7 +198,9 @@ module HomuraRuntime
|
|
|
189
198
|
return ret if ret
|
|
190
199
|
end
|
|
191
200
|
else
|
|
192
|
-
|
|
201
|
+
if @method_returns.key?(method_name)
|
|
202
|
+
return @method_returns[method_name]
|
|
203
|
+
end
|
|
193
204
|
end
|
|
194
205
|
nil
|
|
195
206
|
end
|
|
@@ -198,14 +209,17 @@ module HomuraRuntime
|
|
|
198
209
|
return {} unless durable_object_define_call?(call_node)
|
|
199
210
|
return {} unless args_node&.type == :args
|
|
200
211
|
|
|
201
|
-
arg_names =
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
212
|
+
arg_names =
|
|
213
|
+
args_node.children.filter_map do |arg|
|
|
214
|
+
next unless arg&.type == :arg
|
|
215
|
+
arg.children[0]
|
|
216
|
+
end
|
|
205
217
|
return {} if arg_names.empty?
|
|
206
218
|
|
|
207
|
-
bindings = { arg_names[0] =>
|
|
208
|
-
bindings[
|
|
219
|
+
bindings = { arg_names[0] => "Cloudflare::DurableObjectState" }
|
|
220
|
+
bindings[
|
|
221
|
+
arg_names[1]
|
|
222
|
+
] = "Cloudflare::DurableObjectRequest" if arg_names.length > 1
|
|
209
223
|
bindings
|
|
210
224
|
end
|
|
211
225
|
|
|
@@ -213,7 +227,8 @@ module HomuraRuntime
|
|
|
213
227
|
return false unless call_node&.type == :send
|
|
214
228
|
|
|
215
229
|
receiver, method_name = *call_node
|
|
216
|
-
method_name == :define &&
|
|
230
|
+
method_name == :define &&
|
|
231
|
+
const_path(receiver) == "Cloudflare::DurableObject"
|
|
217
232
|
end
|
|
218
233
|
|
|
219
234
|
def infer_index_class(node)
|
|
@@ -273,7 +288,7 @@ module HomuraRuntime
|
|
|
273
288
|
parts.unshift(n.children[1])
|
|
274
289
|
n = n.children[0]
|
|
275
290
|
end
|
|
276
|
-
parts.join(
|
|
291
|
+
parts.join("::")
|
|
277
292
|
end
|
|
278
293
|
|
|
279
294
|
def debug(msg)
|