homura-runtime 0.1.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.
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ // Phase 11A — rewrite Opal runtime `eval(...)` calls to `globalThis.eval(...)`.
3
+ //
4
+ // The Opal stdlib includes an `eval` / `require_remote` code path used
5
+ // only for interactive / IRB scenarios (Opal.compile → eval at
6
+ // runtime). On Workers we pre-compile every .rb file at build time, so
7
+ // those eval sites never actually fire. esbuild still sees them
8
+ // lexically and emits a "direct eval" warning (which wrangler echoes
9
+ // as red `ERROR` rows). The calls are semantically identical to
10
+ // `globalThis.eval(...)` in these positions (they don't depend on the
11
+ // enclosing lexical scope), and `globalThis.eval` is the
12
+ // spec-blessed way to ask for indirect eval — which esbuild doesn't
13
+ // warn about.
14
+ //
15
+ // Phase 15-A: generic CLI — pass bundle path as first positional or
16
+ // `--input PATH` (defaults to build/hello.no-exit.mjs).
17
+ //
18
+ // This script is invoked as a post-opal step by `npm run build:opal`
19
+ // (via the build pipeline). Runs fast (single regex pass) and is
20
+ // idempotent (re-running on already-patched output is a no-op).
21
+ //
22
+ // We match `eval(` only when preceded by a non-identifier character
23
+ // so we don't rewrite `Kernel#$eval`, `instance_eval`, `module_eval`,
24
+ // `self.$eval`, etc. — those are property accesses / method names,
25
+ // not the global function.
26
+
27
+ import { readFileSync, writeFileSync } from "node:fs";
28
+ import { parseArgs } from "node:util";
29
+
30
+ const { values, positionals } = parseArgs({
31
+ args: process.argv.slice(2),
32
+ options: {
33
+ input: { type: "string", short: "i" },
34
+ },
35
+ allowPositionals: true,
36
+ });
37
+
38
+ const path =
39
+ values.input ||
40
+ positionals[0] ||
41
+ process.env.HOMURA_OPAL_PATCH_INPUT ||
42
+ "build/hello.no-exit.mjs";
43
+
44
+ const src = readFileSync(path, "utf8");
45
+
46
+ // Boundary class: must not be [.$a-zA-Z0-9_]. We include
47
+ // `\b(?<!globalThis\.)` negative lookbehind isn't supported everywhere,
48
+ // so we instead do a two-step: first replace `(^|[^...])eval(` then
49
+ // revert any accidental double-rewrite of `globalThis.eval(`.
50
+ const before = /(^|[^.$a-zA-Z0-9_])eval\(/gm;
51
+ const after = "$1globalThis.eval(";
52
+ let out = src.replace(before, after);
53
+
54
+ // Guard against accidental `globalThis.globalThis.eval(` if this script
55
+ // is run twice (defensive — replace() above is already idempotent
56
+ // because `.globalThis.eval(` starts with `.` which fails the
57
+ // boundary, but belt-and-suspenders).
58
+ out = out.replace(/globalThis\.globalThis\.eval\(/g, "globalThis.eval(");
59
+
60
+ const changes = (out.match(/globalThis\.eval\(/g) || []).length;
61
+ if (out === src) {
62
+ console.log(`[patch-opal-evals] no changes needed (${path})`);
63
+ } else {
64
+ writeFileSync(path, out);
65
+ console.log(`[patch-opal-evals] rewrote ${changes} direct eval → globalThis.eval (${path})`);
66
+ }
@@ -0,0 +1,20 @@
1
+ // Phase 7 — bootstrap that exposes node:crypto on globalThis so the
2
+ // Opal-compiled Ruby bundle can use synchronous hash / hmac / cipher /
3
+ // pkey / kdf APIs without async/await glue.
4
+ //
5
+ // Cloudflare Workers exposes node:crypto when `compatibility_flags`
6
+ // includes "nodejs_compat" (already enabled in wrangler.toml). Node
7
+ // itself ships node:crypto natively, so the same import works in:
8
+ //
9
+ // - Production (Cloudflare Workers + nodejs_compat)
10
+ // - Test (Node.js, via `node --import ./gems/homura-runtime/runtime/setup-node-crypto.mjs`)
11
+ //
12
+ // Why globalThis: Opal-emitted ESM modules cannot easily declare new
13
+ // `import` statements after the build. Setting a global lets every
14
+ // Ruby code path reach the same crypto module via a single backtick:
15
+ //
16
+ // `globalThis.__nodeCrypto__.createHash('sha256')`
17
+
18
+ import nodeCrypto from "node:crypto";
19
+
20
+ globalThis.__nodeCrypto__ = nodeCrypto;
@@ -0,0 +1,9 @@
1
+ // Cloudflare Workers Module Worker — thin bootstrap (Phase 15-E).
2
+ // Order: node:crypto shim → Opal bundle side effects → Rack/Queue/DO handlers.
3
+ //
4
+ // Prefer `build/worker.entrypoint.mjs` as wrangler `main` in application repos;
5
+ // this file remains valid for monorepo layouts that point `main` at the gem path.
6
+
7
+ import "./setup-node-crypto.mjs";
8
+ import "../../../build/hello.no-exit.mjs";
9
+ export { default, HomuraCounterDO } from "./worker_module.mjs";
@@ -0,0 +1,384 @@
1
+ // Rack / Cron / Queue / DO adapters for Cloudflare Workers (no Opal bundle import).
2
+ // Load order: setup-node-crypto.mjs → Opal bundle (side effects) → this module.
3
+ // Phase 15-E: split from worker.mjs so generated worker.entrypoint.mjs can use
4
+ // fixed relative imports to the build artifact.
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Phase 15-A — dispatch resolution: prefer __OPAL_WORKERS__, alias legacy
8
+ // ---------------------------------------------------------------------------
9
+
10
+ function rackDispatch() {
11
+ const ow = globalThis.__OPAL_WORKERS__;
12
+ const fn = ow && typeof ow.rack === "function" ? ow.rack : undefined;
13
+ return fn || globalThis.__HOMURA_RACK_DISPATCH__;
14
+ }
15
+
16
+ /** Phase 17 — Workers `env.SEND_EMAIL` を Rack 構築より前に global に載せ、Miniflare でも Ruby が同じバインディングを拾えるようにする。 */
17
+ function ensureOpalWorkersEmailBinding(env) {
18
+ const ow = (globalThis.__OPAL_WORKERS__ ||= {});
19
+ if (env && env.SEND_EMAIL) {
20
+ ow.sendEmailBinding = env.SEND_EMAIL;
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Miniflare の entry.worker は `/cdn-cgi/mf/scheduled` だけ先に処理する。
26
+ * `/cdn-cgi/handler/email` 等はユーザ Worker の fetch に届くため、ここで Rack に渡さず処理する。
27
+ * (Cloudflare Email Routing — Local Development の受信スタブ向け。Phase 18 で `email()` と接続予定)
28
+ */
29
+ async function handleCdnCgiBypass(request, env, _ctx) {
30
+ const url = new URL(request.url);
31
+ if (url.pathname === "/cdn-cgi/handler/email") {
32
+ if (request.method === "POST") {
33
+ try {
34
+ globalThis.console.log(
35
+ "[homura] POST /cdn-cgi/handler/email — bypass Rack (Email Routing local-dev stub path)",
36
+ );
37
+ } catch (_e) {}
38
+ return new Response(
39
+ JSON.stringify({
40
+ ok: true,
41
+ bypass: "rack",
42
+ pathname: url.pathname,
43
+ note: "homura worker_module forwards /cdn-cgi away from Sinatra. Full inbound handling: Phase 18 (export email()).",
44
+ }),
45
+ { status: 200, headers: { "content-type": "application/json; charset=utf-8" } },
46
+ );
47
+ }
48
+ return new Response("Method Not Allowed", { status: 405 });
49
+ }
50
+ if (env.ASSETS && typeof env.ASSETS.fetch === "function") {
51
+ return env.ASSETS.fetch(request);
52
+ }
53
+ return new Response("not handled", { status: 404, headers: { "content-type": "text/plain; charset=utf-8" } });
54
+ }
55
+
56
+ function scheduledDispatch() {
57
+ const ow = globalThis.__OPAL_WORKERS__;
58
+ const fn = ow && typeof ow.scheduled === "function" ? ow.scheduled : undefined;
59
+ return fn || globalThis.__HOMURA_SCHEDULED_DISPATCH__;
60
+ }
61
+
62
+ function queueDispatch() {
63
+ const ow = globalThis.__OPAL_WORKERS__;
64
+ const fn = ow && typeof ow.queue === "function" ? ow.queue : undefined;
65
+ return fn || globalThis.__HOMURA_QUEUE_DISPATCH__;
66
+ }
67
+
68
+ function durableObjectDispatch() {
69
+ const d = globalThis.__OPAL_WORKERS__ && globalThis.__OPAL_WORKERS__.durableObject;
70
+ const fn = d && typeof d.dispatch === "function" ? d.dispatch : undefined;
71
+ return fn || globalThis.__HOMURA_DO_DISPATCH__;
72
+ }
73
+
74
+ function durableObjectWsMessage() {
75
+ const d = globalThis.__OPAL_WORKERS__ && globalThis.__OPAL_WORKERS__.durableObject;
76
+ const fn = d && typeof d.wsMessage === "function" ? d.wsMessage : undefined;
77
+ return fn || globalThis.__HOMURA_DO_WS_MESSAGE__;
78
+ }
79
+
80
+ function durableObjectWsClose() {
81
+ const d = globalThis.__OPAL_WORKERS__ && globalThis.__OPAL_WORKERS__.durableObject;
82
+ const fn = d && typeof d.wsClose === "function" ? d.wsClose : undefined;
83
+ return fn || globalThis.__HOMURA_DO_WS_CLOSE__;
84
+ }
85
+
86
+ function durableObjectWsError() {
87
+ const d = globalThis.__OPAL_WORKERS__ && globalThis.__OPAL_WORKERS__.durableObject;
88
+ const fn = d && typeof d.wsError === "function" ? d.wsError : undefined;
89
+ return fn || globalThis.__HOMURA_DO_WS_ERROR__;
90
+ }
91
+
92
+ // Phase 11A — binary-safe body passthrough. Convert an ArrayBuffer of
93
+ // request body bytes into a latin1 String (each code unit 0–255 is
94
+ // exactly one byte). Opal Strings are JS Strings (UTF-16), but a
95
+ // latin1-encoded string survives through StringIO / Ruby unchanged.
96
+ // The Ruby side (`Cloudflare::Multipart`) reads those bytes and, for
97
+ // file parts, converts them back to a real Uint8Array when passing
98
+ // to R2.put / fetch body.
99
+ //
100
+ // Chunked to avoid the `Maximum call stack size exceeded` hazard of
101
+ // `String.fromCharCode.apply(null, hugeArray)` — the ~0xFFFE arg cap
102
+ // differs per engine, so we stay comfortably below at 0x8000.
103
+ function binaryArrayBufferToLatin1String(arrayBuffer) {
104
+ const u8 = new Uint8Array(arrayBuffer);
105
+ const CHUNK = 0x8000;
106
+ // Accumulate into an array and join once at the end — in-loop
107
+ // String concatenation is O(n²) on V8 for large uploads. Each
108
+ // `chunk` here is already a small String (≤ 32768 chars), so
109
+ // join() can reuse rope structures efficiently.
110
+ const parts = [];
111
+ for (let i = 0; i < u8.length; i += CHUNK) {
112
+ const slice = u8.subarray(i, Math.min(i + CHUNK, u8.length));
113
+ parts.push(String.fromCharCode.apply(null, slice));
114
+ }
115
+ return parts.join("");
116
+ }
117
+
118
+ // Module Worker `fetch` and Durable Object HTTP `fetch` must use the same body
119
+ // semantics: multipart/form-data uses arrayBuffer + latin1 (Phase 11A), not UTF-8 text().
120
+ async function readBodyTextForRubyDispatcher(request) {
121
+ const method = (request && request.method ? request.method : "GET").toUpperCase();
122
+ if (method === "GET" || method === "HEAD" || method === "OPTIONS") {
123
+ return { bodyText: "" };
124
+ }
125
+ try {
126
+ const contentType = request.headers.get("content-type") || "";
127
+ if (contentType.toLowerCase().includes("multipart/")) {
128
+ const buf = await request.arrayBuffer();
129
+ return { bodyText: binaryArrayBufferToLatin1String(buf) };
130
+ }
131
+ return { bodyText: await request.text() };
132
+ } catch (err) {
133
+ return {
134
+ bodyReadError: new Response(
135
+ JSON.stringify({ error: "failed to read request body", detail: err.message }),
136
+ { status: 400, headers: { "content-type": "application/json" } },
137
+ ),
138
+ };
139
+ }
140
+ }
141
+
142
+ export default {
143
+ async fetch(request, env, ctx) {
144
+ ensureOpalWorkersEmailBinding(env);
145
+
146
+ let reqUrl;
147
+ try {
148
+ reqUrl = new URL(request.url);
149
+ } catch (_e) {
150
+ reqUrl = { pathname: "/" };
151
+ }
152
+ if (reqUrl.pathname.startsWith("/cdn-cgi/")) {
153
+ return handleCdnCgiBypass(request, env, ctx);
154
+ }
155
+
156
+ const dispatch = rackDispatch();
157
+ if (typeof dispatch !== "function") {
158
+ return new Response(
159
+ "homura: Rack dispatcher not installed (Rack::Handler::CloudflareWorkers.run never called)\n",
160
+ { status: 500, headers: { "content-type": "text/plain; charset=utf-8" } },
161
+ );
162
+ }
163
+
164
+ // Read the body here while we still have the async context. The Opal
165
+ // side runs synchronous Ruby, so it cannot `await req.text()` inside
166
+ // the dispatcher. For methods that are defined to have no body
167
+ // (GET, HEAD, OPTIONS by spec) we skip the read entirely to avoid
168
+ // wasting a round-trip.
169
+ //
170
+ const bodyResult = await readBodyTextForRubyDispatcher(request);
171
+ if (bodyResult.bodyReadError) {
172
+ return bodyResult.bodyReadError;
173
+ }
174
+
175
+ return dispatch(request, env, ctx, bodyResult.bodyText);
176
+ },
177
+
178
+ // Phase 9 — Cloudflare Workers Cron Triggers entry point.
179
+ //
180
+ // The Workers runtime invokes this `scheduled` export once per
181
+ // matching `[triggers] crons` entry in wrangler.toml. We forward
182
+ // the (event, env, ctx) triple to the Ruby-side dispatcher
183
+ // installed by `lib/cloudflare_workers/scheduled.rb`
184
+ // (which registers `globalThis.__HOMURA_SCHEDULED_DISPATCH__`).
185
+ //
186
+ // The Ruby dispatcher walks every job registered via the
187
+ // Sinatra DSL `schedule '*/5 * * * *' do ... end` and runs
188
+ // each one whose cron pattern matches `event.cron`.
189
+ //
190
+ // Local manual triggering (no cron poll wait):
191
+ // wrangler dev --test-scheduled
192
+ // curl 'http://127.0.0.1:8787/__scheduled?cron=*/5+*+*+*+*'
193
+ //
194
+ // Tip: ALWAYS call ctx.waitUntil on long-running work so the
195
+ // Workers runtime keeps the isolate alive past the dispatcher's
196
+ // synchronous return. The Ruby helper `wait_until(promise)` does
197
+ // exactly that.
198
+ async scheduled(event, env, ctx) {
199
+ const dispatch = scheduledDispatch();
200
+ if (typeof dispatch !== "function") {
201
+ // No Ruby dispatcher installed — the Opal bundle didn't
202
+ // require 'cloudflare_workers/scheduled'. Log loudly so this
203
+ // misconfiguration surfaces instead of silently dropping
204
+ // every cron firing.
205
+ try {
206
+ globalThis.console.error(
207
+ "homura: scheduled dispatcher not installed (require 'cloudflare_workers/scheduled' missing)",
208
+ );
209
+ } catch (e) {
210
+ // ignore — console may itself be broken in pathological cases
211
+ }
212
+ return;
213
+ }
214
+
215
+ // Hand the work to ctx.waitUntil so an async Ruby dispatcher
216
+ // (D1 writes, KV writes, fetch calls) can finish even though
217
+ // `scheduled` returns a Promise the runtime may not fully await
218
+ // on its own.
219
+ const work = (async () => {
220
+ try {
221
+ return await dispatch(event, env, ctx);
222
+ } catch (err) {
223
+ try {
224
+ globalThis.console.error("[scheduled] dispatcher threw:", err && err.stack || err);
225
+ } catch (e) {
226
+ // ignore
227
+ }
228
+ }
229
+ })();
230
+ ctx.waitUntil(work);
231
+ return work;
232
+ },
233
+
234
+ // Phase 11B — Cloudflare Queues consumer entry point.
235
+ //
236
+ // The Workers runtime invokes this `queue` export once per batch
237
+ // for every `[[queues.consumers]]` entry in wrangler.toml. The
238
+ // Ruby side registers handlers through the `consume_queue
239
+ // 'queue-name' do |batch| ... end` DSL (lib/sinatra/queue.rb);
240
+ // `globalThis.__HOMURA_QUEUE_DISPATCH__` walks `batch.queue`
241
+ // against those handlers and runs whichever matches. A bad handler
242
+ // doesn't crash the consumer — errors are caught and logged so
243
+ // sibling handlers still run.
244
+ async queue(batch, env, ctx) {
245
+ const dispatch = queueDispatch();
246
+ if (typeof dispatch !== "function") {
247
+ try {
248
+ globalThis.console.error(
249
+ "homura: queue dispatcher not installed (require 'cloudflare_workers/queue' missing)",
250
+ );
251
+ } catch (e) {}
252
+ return;
253
+ }
254
+ const work = (async () => {
255
+ try {
256
+ return await dispatch(batch, env, ctx);
257
+ } catch (err) {
258
+ try {
259
+ globalThis.console.error("[queue] dispatcher threw:", err && err.stack || err);
260
+ } catch (e) {}
261
+ }
262
+ })();
263
+ ctx.waitUntil(work);
264
+ return work;
265
+ },
266
+ };
267
+
268
+ // ---------------------------------------------------------------------
269
+ // Phase 11B — Durable Objects entry point.
270
+ //
271
+ // Every DO class listed in wrangler.toml's `[[durable_objects.bindings]]`
272
+ // must be a named export on this module. We export ONE generic class
273
+ // (`HomuraCounterDO`) that forwards every `fetch(req)` it receives
274
+ // to the Ruby-side dispatcher installed by
275
+ // `lib/cloudflare_workers/durable_object.rb`
276
+ // (`globalThis.__HOMURA_DO_DISPATCH__`).
277
+ //
278
+ // The Ruby handler is registered via:
279
+ //
280
+ // Cloudflare::DurableObject.define('HomuraCounterDO') do |state, req|
281
+ // ...
282
+ // end
283
+ //
284
+ // Keep this JS class as minimal as possible — every piece of DO logic
285
+ // lives in Ruby. When a new DO class is needed in a later phase, add
286
+ // another exported class next to this one with the same dispatcher
287
+ // forwarding and a different `class_name` argument.
288
+ // ---------------------------------------------------------------------
289
+
290
+ async function __homuraForwardDO(class_name, state, env, request) {
291
+ // Pre-await the request body so the Ruby dispatcher (which runs
292
+ // synchronously under Opal) can read it without its own await.
293
+ // Multipart uses the same byte-preserving path as Module Worker fetch (Phase 11A).
294
+ const bodyResult = await readBodyTextForRubyDispatcher(request);
295
+ if (bodyResult.bodyReadError) {
296
+ return bodyResult.bodyReadError;
297
+ }
298
+ const bodyText = bodyResult.bodyText;
299
+ const dispatch = durableObjectDispatch();
300
+ if (typeof dispatch !== "function") {
301
+ return new Response(
302
+ JSON.stringify({ error: "homura: DO dispatcher not installed" }),
303
+ { status: 500, headers: { "content-type": "application/json" } },
304
+ );
305
+ }
306
+ return await dispatch(class_name, state, env, request, bodyText);
307
+ }
308
+
309
+ export class HomuraCounterDO {
310
+ constructor(state, env) {
311
+ this.state = state;
312
+ this.env = env;
313
+ }
314
+ async fetch(request) {
315
+ // Upgrade path: if the request asks for a WebSocket, accept one
316
+ // end of a new pair via the Hibernation API and hand the other
317
+ // end back to the client as part of a 101 response. After this,
318
+ // the runtime dispatches any subsequent frames on the server
319
+ // socket through the `webSocketMessage` / `webSocketClose` /
320
+ // `webSocketError` methods below (which forward to Ruby).
321
+ if (request.headers.get("Upgrade")?.toLowerCase() === "websocket") {
322
+ const pair = new WebSocketPair();
323
+ // The server end hibernates with the DO; tag it with the path
324
+ // so Ruby handlers can filter.
325
+ let url;
326
+ try { url = new URL(request.url); } catch (_e) { url = { pathname: "/" }; }
327
+ const tag = "path:" + (url.pathname || "/");
328
+ try {
329
+ this.state.acceptWebSocket(pair[1], [tag]);
330
+ } catch (_e) {
331
+ // Runtimes without Hibernation API — fall back to accepting
332
+ // manually AND attaching event listeners that forward frames
333
+ // to the same Ruby-side dispatchers `webSocketMessage` /
334
+ // `webSocketClose` / `webSocketError` use. Without these the
335
+ // upgrade would succeed but messages would silently drop
336
+ // (Copilot review PR #9, fourth pass).
337
+ try { pair[1].accept(); } catch (_) {}
338
+ const self = this;
339
+ pair[1].addEventListener("message", async (ev) => {
340
+ const fn = durableObjectWsMessage();
341
+ if (typeof fn === "function") {
342
+ try { await fn("HomuraCounterDO", pair[1], ev.data, self.state, self.env); } catch (_) {}
343
+ }
344
+ });
345
+ pair[1].addEventListener("close", async (ev) => {
346
+ const fn = durableObjectWsClose();
347
+ if (typeof fn === "function") {
348
+ try { await fn("HomuraCounterDO", pair[1], ev.code, ev.reason, ev.wasClean, self.state, self.env); } catch (_) {}
349
+ }
350
+ });
351
+ pair[1].addEventListener("error", async (ev) => {
352
+ const fn = durableObjectWsError();
353
+ if (typeof fn === "function") {
354
+ try { await fn("HomuraCounterDO", pair[1], ev, self.state, self.env); } catch (_) {}
355
+ }
356
+ });
357
+ }
358
+ return new Response(null, { status: 101, webSocket: pair[0] });
359
+ }
360
+ return __homuraForwardDO("HomuraCounterDO", this.state, this.env, request);
361
+ }
362
+
363
+ // Hibernation API callbacks — routed into Ruby via the hooks
364
+ // installed by `lib/cloudflare_workers/durable_object.rb`. Each
365
+ // hook is optional on the Ruby side; missing hooks are a no-op.
366
+ async webSocketMessage(ws, message) {
367
+ const fn = durableObjectWsMessage();
368
+ if (typeof fn === "function") {
369
+ return fn("HomuraCounterDO", ws, message, this.state, this.env);
370
+ }
371
+ }
372
+ async webSocketClose(ws, code, reason, wasClean) {
373
+ const fn = durableObjectWsClose();
374
+ if (typeof fn === "function") {
375
+ return fn("HomuraCounterDO", ws, code, reason, wasClean, this.state, this.env);
376
+ }
377
+ }
378
+ async webSocketError(ws, err) {
379
+ const fn = durableObjectWsError();
380
+ if (typeof fn === "function") {
381
+ return fn("HomuraCounterDO", ws, err, this.state, this.env);
382
+ }
383
+ }
384
+ }
@@ -0,0 +1,40 @@
1
+ # Example wrangler fragment for apps using cloudflare-workers-runtime.
2
+ # Copy patterns into your own wrangler.toml and set:
3
+ # main = "gems/homura-runtime/runtime/worker.mjs"
4
+ # (adjust path if you vendor the gem elsewhere.)
5
+ #
6
+ # name = "my-opal-worker"
7
+ # main = "gems/homura-runtime/runtime/worker.mjs"
8
+ # compatibility_date = "2026-04-15"
9
+ # compatibility_flags = ["nodejs_compat"]
10
+ #
11
+ # --- Optional bindings (uncomment what you use) ---
12
+ #
13
+ # [[d1_databases]]
14
+ # binding = "DB"
15
+ # database_name = "my-db"
16
+ # database_id = "REPLACE_ME"
17
+ #
18
+ # [[kv_namespaces]]
19
+ # binding = "KV"
20
+ # id = "REPLACE_ME"
21
+ #
22
+ # [[r2_buckets]]
23
+ # binding = "BUCKET"
24
+ # bucket_name = "my-bucket"
25
+ #
26
+ # [ai]
27
+ # binding = "AI"
28
+ #
29
+ # [[durable_objects.bindings]]
30
+ # name = "MY_DO"
31
+ # class_name = "MyDurableObjectClass"
32
+ #
33
+ # [[queues.producers]]
34
+ # binding = "MY_QUEUE"
35
+ # queue = "my-queue"
36
+ #
37
+ # [[queues.consumers]]
38
+ # queue = "my-queue"
39
+ # max_batch_size = 10
40
+ # max_batch_timeout = 5
@@ -0,0 +1,8 @@
1
+ # Minimal Consumer wrangler.toml — paste into your Worker project.
2
+ # Phase 17: uncomment to enable Cloudflare Email Service outbound sending.
3
+
4
+ # [[send_email]]
5
+ # name = "SEND_EMAIL"
6
+
7
+ # [vars]
8
+ # HOMURA_MAIL_FROM = "noreply@your-verified-domain.example"
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: homura-runtime
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kazuhiro NISHIYAMA
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: opal-homura
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - '='
17
+ - !ruby/object:Gem::Version
18
+ version: 1.8.3.rc1
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - '='
24
+ - !ruby/object:Gem::Version
25
+ version: 1.8.3.rc1
26
+ - !ruby/object:Gem::Dependency
27
+ name: parser
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.3'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.3'
40
+ description: |
41
+ Sinatra-free core for running Opal-compiled Ruby on Cloudflare Workers:
42
+ Rack handler, D1/KV/R2/AI/Queue/Durable Object adapters, multipart/streaming,
43
+ and Opal corelib patches. Use with the `opal` gem and a Module Worker
44
+ (`runtime/worker.mjs` in this gem).
45
+ executables:
46
+ - cloudflare-workers-build
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - CHANGELOG.md
51
+ - README.md
52
+ - bin/cloudflare-workers-build
53
+ - docs/ARCHITECTURE.md
54
+ - exe/auto-await
55
+ - exe/compile-assets
56
+ - exe/compile-erb
57
+ - lib/cloudflare_workers.rb
58
+ - lib/cloudflare_workers/ai.rb
59
+ - lib/cloudflare_workers/async_registry.rb
60
+ - lib/cloudflare_workers/auto_await/analyzer.rb
61
+ - lib/cloudflare_workers/auto_await/transformer.rb
62
+ - lib/cloudflare_workers/cache.rb
63
+ - lib/cloudflare_workers/durable_object.rb
64
+ - lib/cloudflare_workers/email.rb
65
+ - lib/cloudflare_workers/http.rb
66
+ - lib/cloudflare_workers/multipart.rb
67
+ - lib/cloudflare_workers/queue.rb
68
+ - lib/cloudflare_workers/scheduled.rb
69
+ - lib/cloudflare_workers/stream.rb
70
+ - lib/cloudflare_workers/version.rb
71
+ - lib/opal_patches.rb
72
+ - runtime/patch-opal-evals.mjs
73
+ - runtime/setup-node-crypto.mjs
74
+ - runtime/worker.mjs
75
+ - runtime/worker_module.mjs
76
+ - runtime/wrangler.toml.example
77
+ - templates/wrangler.toml.example
78
+ homepage: https://github.com/kazuph/homura
79
+ licenses:
80
+ - MIT
81
+ metadata:
82
+ homepage_uri: https://github.com/kazuph/homura
83
+ source_code_uri: https://github.com/kazuph/homura/tree/main/gems/homura-runtime
84
+ bug_tracker_uri: https://github.com/kazuph/homura/issues
85
+ changelog_uri: https://github.com/kazuph/homura/blob/main/gems/homura-runtime/CHANGELOG.md
86
+ readme_uri: https://github.com/kazuph/homura/blob/main/gems/homura-runtime/README.md
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: 3.4.0
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ requirements: []
101
+ rubygems_version: 3.6.9
102
+ specification_version: 4
103
+ summary: Cloudflare Workers + Opal runtime core (Rack dispatch, bindings, patches)
104
+ test_files: []