homura-runtime 0.3.6 → 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 +11 -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,13 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "pathname"
|
|
5
5
|
|
|
6
6
|
module HomuraRuntime
|
|
7
7
|
module BuildSupport
|
|
8
|
-
RUNTIME_GEM_NAME =
|
|
9
|
-
SINATRA_GEM_NAME =
|
|
10
|
-
SEQUEL_D1_GEM_NAME =
|
|
8
|
+
RUNTIME_GEM_NAME = "homura-runtime"
|
|
9
|
+
SINATRA_GEM_NAME = "sinatra-homura"
|
|
10
|
+
SEQUEL_D1_GEM_NAME = "sequel-d1"
|
|
11
11
|
|
|
12
12
|
class << self
|
|
13
13
|
def loaded_spec(name, loaded_specs: Gem.loaded_specs)
|
|
@@ -18,22 +18,24 @@ module HomuraRuntime
|
|
|
18
18
|
spec = loaded_spec(name, loaded_specs: loaded_specs)
|
|
19
19
|
return spec.full_gem_path if spec
|
|
20
20
|
|
|
21
|
-
raise(
|
|
21
|
+
raise(
|
|
22
|
+
"homura build: gem #{name} not loaded; use bundle exec from app root"
|
|
23
|
+
)
|
|
22
24
|
end
|
|
23
25
|
|
|
24
26
|
def runtime_root(current_file:, loaded_specs: Gem.loaded_specs)
|
|
25
27
|
spec = loaded_spec(RUNTIME_GEM_NAME, loaded_specs: loaded_specs)
|
|
26
28
|
return Pathname(spec.full_gem_path) if spec
|
|
27
29
|
|
|
28
|
-
Pathname(current_file).expand_path.join(
|
|
30
|
+
Pathname(current_file).expand_path.join("../..")
|
|
29
31
|
end
|
|
30
32
|
|
|
31
33
|
def gem_lib(name, loaded_specs: Gem.loaded_specs)
|
|
32
|
-
File.join(gem_root(name, loaded_specs: loaded_specs),
|
|
34
|
+
File.join(gem_root(name, loaded_specs: loaded_specs), "lib")
|
|
33
35
|
end
|
|
34
36
|
|
|
35
37
|
def gem_vendor(name, loaded_specs: Gem.loaded_specs)
|
|
36
|
-
vendor = File.join(gem_root(name, loaded_specs: loaded_specs),
|
|
38
|
+
vendor = File.join(gem_root(name, loaded_specs: loaded_specs), "vendor")
|
|
37
39
|
return vendor if Dir.exist?(vendor)
|
|
38
40
|
|
|
39
41
|
nil
|
|
@@ -43,64 +45,82 @@ module HomuraRuntime
|
|
|
43
45
|
spec = loaded_spec(name, loaded_specs: loaded_specs)
|
|
44
46
|
return nil unless spec
|
|
45
47
|
|
|
46
|
-
File.join(spec.full_gem_path,
|
|
48
|
+
File.join(spec.full_gem_path, "lib")
|
|
47
49
|
end
|
|
48
50
|
|
|
49
51
|
def maybe_gem_vendor(name, loaded_specs: Gem.loaded_specs)
|
|
50
52
|
spec = loaded_spec(name, loaded_specs: loaded_specs)
|
|
51
53
|
return nil unless spec
|
|
52
54
|
|
|
53
|
-
vendor = File.join(spec.full_gem_path,
|
|
55
|
+
vendor = File.join(spec.full_gem_path, "vendor")
|
|
54
56
|
return vendor if Dir.exist?(vendor)
|
|
55
57
|
|
|
56
58
|
nil
|
|
57
59
|
end
|
|
58
60
|
|
|
59
|
-
def runtime_file(
|
|
60
|
-
|
|
61
|
+
def runtime_file(
|
|
62
|
+
*names,
|
|
63
|
+
current_file: __FILE__,
|
|
64
|
+
loaded_specs: Gem.loaded_specs
|
|
65
|
+
)
|
|
66
|
+
runtime_root(
|
|
67
|
+
current_file: current_file,
|
|
68
|
+
loaded_specs: loaded_specs
|
|
69
|
+
).join("runtime", *names)
|
|
61
70
|
end
|
|
62
71
|
|
|
63
|
-
def ensure_standalone_runtime(
|
|
72
|
+
def ensure_standalone_runtime(
|
|
73
|
+
project_root,
|
|
74
|
+
current_file: __FILE__,
|
|
75
|
+
loaded_specs: Gem.loaded_specs
|
|
76
|
+
)
|
|
64
77
|
# The homura runtime needs two .mjs glue files alongside the
|
|
65
78
|
# generated `worker.entrypoint.mjs`. Until 0.2.22 we wrote them
|
|
66
79
|
# to `cf-runtime/` at the project root, which made every Ruby
|
|
67
80
|
# repo carry two opaque JS files in source control. Hide them
|
|
68
81
|
# under `build/cf-runtime/` so the build artifact tree owns
|
|
69
82
|
# them — `build/` is already in the example .gitignore template.
|
|
70
|
-
target_dir = Pathname(project_root).join(
|
|
83
|
+
target_dir = Pathname(project_root).join("build", "cf-runtime")
|
|
71
84
|
FileUtils.mkdir_p(target_dir)
|
|
72
85
|
|
|
73
86
|
%w[setup-node-crypto.mjs worker_module.mjs].each do |name|
|
|
74
|
-
FileUtils.cp(
|
|
87
|
+
FileUtils.cp(
|
|
88
|
+
runtime_file(
|
|
89
|
+
name,
|
|
90
|
+
current_file: current_file,
|
|
91
|
+
loaded_specs: loaded_specs
|
|
92
|
+
),
|
|
93
|
+
target_dir.join(name)
|
|
94
|
+
)
|
|
75
95
|
end
|
|
76
96
|
|
|
77
97
|
target_dir
|
|
78
98
|
end
|
|
79
99
|
|
|
80
|
-
def standalone_load_paths(
|
|
100
|
+
def standalone_load_paths(
|
|
101
|
+
project_root,
|
|
102
|
+
with_db:,
|
|
103
|
+
loaded_specs: Gem.loaded_specs
|
|
104
|
+
)
|
|
81
105
|
root = Pathname(project_root)
|
|
82
106
|
load_paths = []
|
|
83
107
|
|
|
84
108
|
hv = vendor_from_gemfile(root)
|
|
85
109
|
load_paths << hv.to_s if hv
|
|
86
110
|
|
|
87
|
-
load_paths += [
|
|
111
|
+
load_paths += %w[. build/auto_await build/auto_await/app app]
|
|
88
112
|
[
|
|
89
113
|
gem_lib(RUNTIME_GEM_NAME, loaded_specs: loaded_specs),
|
|
90
114
|
gem_vendor(RUNTIME_GEM_NAME, loaded_specs: loaded_specs),
|
|
91
115
|
maybe_gem_lib(SINATRA_GEM_NAME, loaded_specs: loaded_specs),
|
|
92
116
|
maybe_gem_vendor(SINATRA_GEM_NAME, loaded_specs: loaded_specs)
|
|
93
|
-
].compact.each
|
|
94
|
-
load_paths << path
|
|
95
|
-
end
|
|
117
|
+
].compact.each { |path| load_paths << path }
|
|
96
118
|
|
|
97
119
|
if with_db
|
|
98
120
|
[
|
|
99
121
|
gem_vendor(SEQUEL_D1_GEM_NAME, loaded_specs: loaded_specs),
|
|
100
122
|
gem_lib(SEQUEL_D1_GEM_NAME, loaded_specs: loaded_specs)
|
|
101
|
-
].compact.each
|
|
102
|
-
load_paths << path
|
|
103
|
-
end
|
|
123
|
+
].compact.each { |path| load_paths << path }
|
|
104
124
|
end
|
|
105
125
|
|
|
106
126
|
# Pick up any other gems that should ship in the Workers bundle:
|
|
@@ -117,7 +137,8 @@ module HomuraRuntime
|
|
|
117
137
|
# app code.
|
|
118
138
|
opal_gem_paths(root, loaded_specs: loaded_specs).each do |gem_path|
|
|
119
139
|
basename = gem_path.basename.to_s
|
|
120
|
-
rewritten_lib =
|
|
140
|
+
rewritten_lib =
|
|
141
|
+
root.join("build", "auto_await", "gem_#{basename}", "lib")
|
|
121
142
|
load_paths << rewritten_lib.to_s if rewritten_lib.directory?
|
|
122
143
|
%w[lib vendor].each do |sub|
|
|
123
144
|
dir = gem_path.join(sub)
|
|
@@ -125,8 +146,8 @@ module HomuraRuntime
|
|
|
125
146
|
end
|
|
126
147
|
end
|
|
127
148
|
|
|
128
|
-
load_paths <<
|
|
129
|
-
load_paths <<
|
|
149
|
+
load_paths << "vendor" if root.join("vendor").directory?
|
|
150
|
+
load_paths << "build"
|
|
130
151
|
load_paths.uniq
|
|
131
152
|
end
|
|
132
153
|
|
|
@@ -134,20 +155,27 @@ module HomuraRuntime
|
|
|
134
155
|
base = Pathname(project_root).basename.to_s
|
|
135
156
|
parts = base.split(/[^A-Za-z0-9]+/).reject(&:empty?)
|
|
136
157
|
module_name = parts.map { |part| part[0].upcase + part[1..].to_s }.join
|
|
137
|
-
module_name =
|
|
158
|
+
module_name = "App" if module_name.empty?
|
|
138
159
|
module_name = "App#{module_name}" if module_name.match?(/\A\d/)
|
|
139
160
|
"#{module_name}#{suffix}"
|
|
140
161
|
end
|
|
141
162
|
|
|
142
163
|
def vendor_from_gemfile(project_root)
|
|
143
|
-
gf = Pathname(project_root).join(
|
|
164
|
+
gf = Pathname(project_root).join("Gemfile")
|
|
144
165
|
return unless gf.file?
|
|
145
166
|
|
|
146
167
|
txt = gf.read
|
|
147
|
-
|
|
168
|
+
unless (
|
|
169
|
+
m =
|
|
170
|
+
txt.match(
|
|
171
|
+
/#{Regexp.escape(RUNTIME_GEM_NAME)}['"]\s*,\s*path:\s*['"]([^'"]+)['"]/
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
return
|
|
175
|
+
end
|
|
148
176
|
|
|
149
177
|
runtime_path = Pathname.new(m[1]).expand_path(project_root)
|
|
150
|
-
vend = runtime_path.join(
|
|
178
|
+
vend = runtime_path.join("..", "..", "vendor").expand_path
|
|
151
179
|
vend if vend.directory?
|
|
152
180
|
end
|
|
153
181
|
|
|
@@ -166,8 +194,8 @@ module HomuraRuntime
|
|
|
166
194
|
next if wired.include?(spec.name)
|
|
167
195
|
meta = spec.metadata
|
|
168
196
|
next unless meta.is_a?(Hash)
|
|
169
|
-
flag = meta[
|
|
170
|
-
next unless flag ==
|
|
197
|
+
flag = meta["homura.auto_await"]
|
|
198
|
+
next unless flag == "true" || flag == true
|
|
171
199
|
next if spec.full_gem_path.nil?
|
|
172
200
|
gem_path = Pathname(spec.full_gem_path)
|
|
173
201
|
out << gem_path if gem_path.directory?
|
|
@@ -185,10 +213,17 @@ module HomuraRuntime
|
|
|
185
213
|
# * `require: false` gems (dev tooling like `gem 'rspec', path: ..., require: false`)
|
|
186
214
|
# * gems declared inside `group :development do … end` /
|
|
187
215
|
# `group :test do … end` blocks (they don't ship to production)
|
|
188
|
-
EXCLUDED_GROUPS = %i[
|
|
216
|
+
EXCLUDED_GROUPS = %i[
|
|
217
|
+
development
|
|
218
|
+
test
|
|
219
|
+
dev_test
|
|
220
|
+
development_test
|
|
221
|
+
ci
|
|
222
|
+
tools
|
|
223
|
+
].freeze
|
|
189
224
|
|
|
190
225
|
def path_gemfile_entries(project_root)
|
|
191
|
-
gf = Pathname(project_root).join(
|
|
226
|
+
gf = Pathname(project_root).join("Gemfile")
|
|
192
227
|
return [] unless gf.file?
|
|
193
228
|
|
|
194
229
|
wired = [RUNTIME_GEM_NAME, SINATRA_GEM_NAME, SEQUEL_D1_GEM_NAME]
|
|
@@ -197,14 +232,15 @@ module HomuraRuntime
|
|
|
197
232
|
|
|
198
233
|
gf.read.each_line do |line|
|
|
199
234
|
stripped = line.strip
|
|
200
|
-
next if stripped.empty? || stripped.start_with?(
|
|
235
|
+
next if stripped.empty? || stripped.start_with?("#")
|
|
201
236
|
|
|
202
237
|
if (m = stripped.match(/\Agroup\s+(.+?)\s+do\b/))
|
|
203
|
-
groups =
|
|
238
|
+
groups =
|
|
239
|
+
m[1].scan(/[:'"]([A-Za-z0-9_]+)['"]?/).flatten.map(&:to_sym)
|
|
204
240
|
group_stack.push(groups)
|
|
205
241
|
next
|
|
206
242
|
end
|
|
207
|
-
if stripped ==
|
|
243
|
+
if stripped == "end"
|
|
208
244
|
group_stack.pop unless group_stack.empty?
|
|
209
245
|
next
|
|
210
246
|
end
|
data/lib/homura/runtime/cache.rb
CHANGED
|
@@ -50,7 +50,7 @@ module Cloudflare
|
|
|
50
50
|
attr_reader :operation
|
|
51
51
|
def initialize(message, operation: nil)
|
|
52
52
|
@operation = operation
|
|
53
|
-
super("[Cloudflare::Cache] op=#{operation ||
|
|
53
|
+
super("[Cloudflare::Cache] op=#{operation || "match"}: #{message}")
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
56
|
|
|
@@ -62,7 +62,10 @@ module Cloudflare
|
|
|
62
62
|
# Returns a fresh wrapper each call; the underlying JS object is a
|
|
63
63
|
# singleton per isolate.
|
|
64
64
|
def self.default
|
|
65
|
-
Cache.new(
|
|
65
|
+
Cache.new(
|
|
66
|
+
`(typeof caches !== 'undefined' && caches ? caches.default : null)`,
|
|
67
|
+
"default"
|
|
68
|
+
)
|
|
66
69
|
end
|
|
67
70
|
|
|
68
71
|
# caches.open(name) — named cache partitions. Returns a JS Promise
|
|
@@ -71,12 +74,13 @@ module Cloudflare
|
|
|
71
74
|
# don't re-open the handle.
|
|
72
75
|
def self.open(name)
|
|
73
76
|
name_str = name.to_s
|
|
74
|
-
js_promise =
|
|
77
|
+
js_promise =
|
|
78
|
+
`(typeof caches !== 'undefined' && caches && caches.open ? caches.open(#{name_str}) : Promise.resolve(null))`
|
|
75
79
|
js_cache = js_promise.__await__
|
|
76
80
|
Cache.new(js_cache, name_str)
|
|
77
81
|
end
|
|
78
82
|
|
|
79
|
-
def initialize(js_cache, name =
|
|
83
|
+
def initialize(js_cache, name = "default")
|
|
80
84
|
@js_cache = js_cache
|
|
81
85
|
@name = name.to_s
|
|
82
86
|
end
|
|
@@ -109,19 +113,22 @@ module Cloudflare
|
|
|
109
113
|
# JS Request. Derive the URL from the same shapes supported by
|
|
110
114
|
# `request_to_js` so the `url` field on the returned
|
|
111
115
|
# HTTPResponse is always a real URL.
|
|
112
|
-
url_str =
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
116
|
+
url_str =
|
|
117
|
+
if request_or_url.is_a?(String)
|
|
118
|
+
request_or_url
|
|
119
|
+
elsif defined?(Cloudflare::HTTPResponse) &&
|
|
120
|
+
request_or_url.is_a?(Cloudflare::HTTPResponse)
|
|
121
|
+
request_or_url.url.to_s
|
|
122
|
+
elsif `(#{request_or_url} != null && typeof #{request_or_url} === 'object' && typeof #{request_or_url}.url === 'string')`
|
|
123
|
+
`String(#{request_or_url}.url)`
|
|
124
|
+
else
|
|
125
|
+
request_or_url.to_s
|
|
126
|
+
end
|
|
121
127
|
|
|
122
128
|
# Single-line backtick IIFE — see `put` for the Opal multi-line
|
|
123
129
|
# x-string quirk that silently drops the returned Promise.
|
|
124
|
-
js_promise =
|
|
130
|
+
js_promise =
|
|
131
|
+
`(async function(js, req, Kernel, err_klass) { if (js == null || js === Opal.nil) return null; var cached; try { cached = await js.match(req); } catch (e) { Kernel.$raise(err_klass.$new(e && e.message ? e.message : String(e), Opal.hash({ operation: 'match' }))); } if (cached == null) return null; var text = ''; try { text = await cached.text(); } catch (_) { text = ''; } var hk = []; var hv = []; if (cached.headers && typeof cached.headers.forEach === 'function') { cached.headers.forEach(function(v, k) { hk.push(String(k).toLowerCase()); hv.push(String(v)); }); } return { status: cached.status|0, text: text, hkeys: hk, hvals: hv }; })(#{js}, #{req}, #{Kernel}, #{err_klass})`
|
|
125
132
|
js_result = js_promise.__await__
|
|
126
133
|
return nil if `#{js_result} == null`
|
|
127
134
|
|
|
@@ -135,10 +142,10 @@ module Cloudflare
|
|
|
135
142
|
i += 1
|
|
136
143
|
end
|
|
137
144
|
response_klass.new(
|
|
138
|
-
status:
|
|
145
|
+
status: `#{js_result}.status`,
|
|
139
146
|
headers: h,
|
|
140
|
-
body:
|
|
141
|
-
url:
|
|
147
|
+
body: `#{js_result}.text`,
|
|
148
|
+
url: url_str
|
|
142
149
|
)
|
|
143
150
|
end
|
|
144
151
|
|
|
@@ -203,17 +210,17 @@ module Cloudflare
|
|
|
203
210
|
# support but the impl only covered JS Request-like objects — both
|
|
204
211
|
# the docs AND the code now line up.)
|
|
205
212
|
def request_to_js(request_or_url)
|
|
206
|
-
if request_or_url.is_a?(String)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if defined?(Cloudflare::HTTPResponse) && request_or_url.is_a?(Cloudflare::HTTPResponse)
|
|
213
|
+
return `new Request(#{request_or_url})` if request_or_url.is_a?(String)
|
|
214
|
+
if defined?(Cloudflare::HTTPResponse) &&
|
|
215
|
+
request_or_url.is_a?(Cloudflare::HTTPResponse)
|
|
210
216
|
url_str = request_or_url.url.to_s
|
|
211
217
|
return `new Request(#{url_str})`
|
|
212
218
|
end
|
|
213
219
|
if `(#{request_or_url} != null && typeof #{request_or_url} === 'object' && typeof #{request_or_url}.url === 'string')`
|
|
214
220
|
return request_or_url
|
|
215
221
|
end
|
|
216
|
-
raise ArgumentError,
|
|
222
|
+
raise ArgumentError,
|
|
223
|
+
"Cloudflare::Cache request must be a String URL, Cloudflare::HTTPResponse, or JS Request (got #{request_or_url.class})"
|
|
217
224
|
end
|
|
218
225
|
|
|
219
226
|
def ruby_headers_to_js(hash)
|