lopata 0.0.1
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.
- package/README.md +15 -0
- package/package.json +51 -0
- package/runtime/bindings/ai.ts +132 -0
- package/runtime/bindings/analytics-engine.ts +96 -0
- package/runtime/bindings/browser.ts +64 -0
- package/runtime/bindings/cache.ts +179 -0
- package/runtime/bindings/cf-streams.ts +56 -0
- package/runtime/bindings/container-docker.ts +225 -0
- package/runtime/bindings/container.ts +662 -0
- package/runtime/bindings/crypto-extras.ts +89 -0
- package/runtime/bindings/d1.ts +315 -0
- package/runtime/bindings/do-executor-inprocess.ts +140 -0
- package/runtime/bindings/do-executor-worker.ts +368 -0
- package/runtime/bindings/do-executor.ts +45 -0
- package/runtime/bindings/do-websocket-bridge.ts +70 -0
- package/runtime/bindings/do-worker-entry.ts +220 -0
- package/runtime/bindings/do-worker-env.ts +74 -0
- package/runtime/bindings/durable-object.ts +992 -0
- package/runtime/bindings/email.ts +180 -0
- package/runtime/bindings/html-rewriter.ts +84 -0
- package/runtime/bindings/hyperdrive.ts +130 -0
- package/runtime/bindings/images.ts +381 -0
- package/runtime/bindings/kv.ts +359 -0
- package/runtime/bindings/queue.ts +507 -0
- package/runtime/bindings/r2.ts +759 -0
- package/runtime/bindings/rpc-stub.ts +267 -0
- package/runtime/bindings/scheduled.ts +172 -0
- package/runtime/bindings/service-binding.ts +217 -0
- package/runtime/bindings/static-assets.ts +481 -0
- package/runtime/bindings/websocket-pair.ts +182 -0
- package/runtime/bindings/workflow.ts +858 -0
- package/runtime/bunflare-config.ts +56 -0
- package/runtime/cli/cache.ts +39 -0
- package/runtime/cli/context.ts +105 -0
- package/runtime/cli/d1.ts +163 -0
- package/runtime/cli/dev.ts +392 -0
- package/runtime/cli/kv.ts +84 -0
- package/runtime/cli/queues.ts +109 -0
- package/runtime/cli/r2.ts +140 -0
- package/runtime/cli/traces.ts +251 -0
- package/runtime/cli.ts +102 -0
- package/runtime/config.ts +148 -0
- package/runtime/d1-migrate.ts +37 -0
- package/runtime/dashboard/api.ts +174 -0
- package/runtime/dashboard/app.tsx +220 -0
- package/runtime/dashboard/components/breadcrumb.tsx +16 -0
- package/runtime/dashboard/components/buttons.tsx +13 -0
- package/runtime/dashboard/components/code-block.tsx +5 -0
- package/runtime/dashboard/components/detail-field.tsx +8 -0
- package/runtime/dashboard/components/empty-state.tsx +8 -0
- package/runtime/dashboard/components/filter-input.tsx +11 -0
- package/runtime/dashboard/components/index.ts +16 -0
- package/runtime/dashboard/components/key-value-table.tsx +23 -0
- package/runtime/dashboard/components/modal.tsx +23 -0
- package/runtime/dashboard/components/page-header.tsx +11 -0
- package/runtime/dashboard/components/pill-button.tsx +14 -0
- package/runtime/dashboard/components/refresh-button.tsx +7 -0
- package/runtime/dashboard/components/service-info.tsx +45 -0
- package/runtime/dashboard/components/status-badge.tsx +7 -0
- package/runtime/dashboard/components/table-link.tsx +5 -0
- package/runtime/dashboard/components/table.tsx +26 -0
- package/runtime/dashboard/components.tsx +19 -0
- package/runtime/dashboard/index.html +23 -0
- package/runtime/dashboard/lib.ts +45 -0
- package/runtime/dashboard/rpc/client.ts +20 -0
- package/runtime/dashboard/rpc/handlers/ai.ts +71 -0
- package/runtime/dashboard/rpc/handlers/analytics-engine.ts +53 -0
- package/runtime/dashboard/rpc/handlers/cache.ts +24 -0
- package/runtime/dashboard/rpc/handlers/config.ts +137 -0
- package/runtime/dashboard/rpc/handlers/containers.ts +194 -0
- package/runtime/dashboard/rpc/handlers/d1.ts +84 -0
- package/runtime/dashboard/rpc/handlers/do.ts +117 -0
- package/runtime/dashboard/rpc/handlers/email.ts +82 -0
- package/runtime/dashboard/rpc/handlers/errors.ts +32 -0
- package/runtime/dashboard/rpc/handlers/generations.ts +60 -0
- package/runtime/dashboard/rpc/handlers/kv.ts +76 -0
- package/runtime/dashboard/rpc/handlers/overview.ts +94 -0
- package/runtime/dashboard/rpc/handlers/queue.ts +79 -0
- package/runtime/dashboard/rpc/handlers/r2.ts +72 -0
- package/runtime/dashboard/rpc/handlers/scheduled.ts +91 -0
- package/runtime/dashboard/rpc/handlers/traces.ts +64 -0
- package/runtime/dashboard/rpc/handlers/workers.ts +65 -0
- package/runtime/dashboard/rpc/handlers/workflows.ts +171 -0
- package/runtime/dashboard/rpc/hooks.ts +132 -0
- package/runtime/dashboard/rpc/server.ts +70 -0
- package/runtime/dashboard/rpc/types.ts +396 -0
- package/runtime/dashboard/sql-browser/data-browser-tab.tsx +122 -0
- package/runtime/dashboard/sql-browser/editable-cell.tsx +117 -0
- package/runtime/dashboard/sql-browser/filter-row.tsx +99 -0
- package/runtime/dashboard/sql-browser/history-panels.tsx +110 -0
- package/runtime/dashboard/sql-browser/hooks.ts +137 -0
- package/runtime/dashboard/sql-browser/index.ts +4 -0
- package/runtime/dashboard/sql-browser/insert-row-form.tsx +85 -0
- package/runtime/dashboard/sql-browser/modals.tsx +116 -0
- package/runtime/dashboard/sql-browser/schema-browser-tab.tsx +67 -0
- package/runtime/dashboard/sql-browser/sql-browser.tsx +52 -0
- package/runtime/dashboard/sql-browser/sql-console-tab.tsx +124 -0
- package/runtime/dashboard/sql-browser/table-data-view.tsx +566 -0
- package/runtime/dashboard/sql-browser/table-sidebar.tsx +38 -0
- package/runtime/dashboard/sql-browser/types.ts +61 -0
- package/runtime/dashboard/sql-browser/utils.ts +167 -0
- package/runtime/dashboard/style.css +177 -0
- package/runtime/dashboard/views/ai.tsx +152 -0
- package/runtime/dashboard/views/analytics-engine.tsx +169 -0
- package/runtime/dashboard/views/cache.tsx +93 -0
- package/runtime/dashboard/views/containers.tsx +197 -0
- package/runtime/dashboard/views/d1.tsx +81 -0
- package/runtime/dashboard/views/do.tsx +168 -0
- package/runtime/dashboard/views/email.tsx +235 -0
- package/runtime/dashboard/views/errors.tsx +558 -0
- package/runtime/dashboard/views/home.tsx +287 -0
- package/runtime/dashboard/views/kv.tsx +273 -0
- package/runtime/dashboard/views/queue.tsx +193 -0
- package/runtime/dashboard/views/r2.tsx +202 -0
- package/runtime/dashboard/views/scheduled.tsx +89 -0
- package/runtime/dashboard/views/trace-waterfall.tsx +410 -0
- package/runtime/dashboard/views/traces.tsx +768 -0
- package/runtime/dashboard/views/workers.tsx +55 -0
- package/runtime/dashboard/views/workflows.tsx +473 -0
- package/runtime/db.ts +258 -0
- package/runtime/env.ts +362 -0
- package/runtime/error-page/app.tsx +394 -0
- package/runtime/error-page/build.ts +269 -0
- package/runtime/error-page/index.html +16 -0
- package/runtime/error-page/style.css +31 -0
- package/runtime/execution-context.ts +18 -0
- package/runtime/file-watcher.ts +57 -0
- package/runtime/generation-manager.ts +230 -0
- package/runtime/generation.ts +411 -0
- package/runtime/plugin.ts +292 -0
- package/runtime/request-cf.ts +28 -0
- package/runtime/rpc-validate.ts +154 -0
- package/runtime/tracing/context.ts +40 -0
- package/runtime/tracing/db.ts +73 -0
- package/runtime/tracing/frames.ts +75 -0
- package/runtime/tracing/instrument.ts +186 -0
- package/runtime/tracing/span.ts +138 -0
- package/runtime/tracing/store.ts +499 -0
- package/runtime/tracing/types.ts +47 -0
- package/runtime/vite-plugin/config-plugin.ts +68 -0
- package/runtime/vite-plugin/dev-server-plugin.ts +493 -0
- package/runtime/vite-plugin/dist/index.mjs +52333 -0
- package/runtime/vite-plugin/globals-plugin.ts +94 -0
- package/runtime/vite-plugin/index.ts +43 -0
- package/runtime/vite-plugin/modules-plugin.ts +88 -0
- package/runtime/vite-plugin/react-router-plugin.ts +95 -0
- package/runtime/worker-registry.ts +52 -0
package/runtime/db.ts
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
|
|
5
|
+
const DATA_DIR = join(process.cwd(), ".bunflare");
|
|
6
|
+
const DB_PATH = join(DATA_DIR, "data.sqlite");
|
|
7
|
+
|
|
8
|
+
let instance: Database | null = null;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Returns the shared SQLite database singleton for bunflare runtime data.
|
|
12
|
+
* Creates the .bunflare/ directory and database file on first call.
|
|
13
|
+
*/
|
|
14
|
+
export function getDatabase(): Database {
|
|
15
|
+
if (instance) return instance;
|
|
16
|
+
|
|
17
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
18
|
+
mkdirSync(join(DATA_DIR, "r2"), { recursive: true });
|
|
19
|
+
mkdirSync(join(DATA_DIR, "d1"), { recursive: true });
|
|
20
|
+
|
|
21
|
+
instance = new Database(DB_PATH, { create: true });
|
|
22
|
+
instance.run("PRAGMA journal_mode=WAL");
|
|
23
|
+
runMigrations(instance);
|
|
24
|
+
return instance;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Initialize schema on the given database. Exported so tests and
|
|
29
|
+
* external callers can run migrations on an arbitrary Database instance (e.g. :memory:).
|
|
30
|
+
*/
|
|
31
|
+
export function runMigrations(db: Database): void {
|
|
32
|
+
db.run(`
|
|
33
|
+
CREATE TABLE IF NOT EXISTS kv (
|
|
34
|
+
namespace TEXT NOT NULL,
|
|
35
|
+
key TEXT NOT NULL,
|
|
36
|
+
value BLOB NOT NULL,
|
|
37
|
+
metadata TEXT,
|
|
38
|
+
expiration INTEGER,
|
|
39
|
+
PRIMARY KEY (namespace, key)
|
|
40
|
+
)
|
|
41
|
+
`);
|
|
42
|
+
|
|
43
|
+
db.run(`
|
|
44
|
+
CREATE TABLE IF NOT EXISTS r2_objects (
|
|
45
|
+
bucket TEXT NOT NULL,
|
|
46
|
+
key TEXT NOT NULL,
|
|
47
|
+
size INTEGER NOT NULL,
|
|
48
|
+
etag TEXT NOT NULL,
|
|
49
|
+
version TEXT NOT NULL DEFAULT '',
|
|
50
|
+
uploaded TEXT NOT NULL,
|
|
51
|
+
http_metadata TEXT,
|
|
52
|
+
custom_metadata TEXT,
|
|
53
|
+
checksums TEXT,
|
|
54
|
+
PRIMARY KEY (bucket, key)
|
|
55
|
+
)
|
|
56
|
+
`);
|
|
57
|
+
|
|
58
|
+
db.run(`
|
|
59
|
+
CREATE TABLE IF NOT EXISTS r2_multipart_uploads (
|
|
60
|
+
upload_id TEXT PRIMARY KEY,
|
|
61
|
+
bucket TEXT NOT NULL,
|
|
62
|
+
key TEXT NOT NULL,
|
|
63
|
+
http_metadata TEXT,
|
|
64
|
+
custom_metadata TEXT,
|
|
65
|
+
created_at TEXT NOT NULL
|
|
66
|
+
)
|
|
67
|
+
`);
|
|
68
|
+
|
|
69
|
+
db.run(`
|
|
70
|
+
CREATE TABLE IF NOT EXISTS r2_multipart_parts (
|
|
71
|
+
upload_id TEXT NOT NULL,
|
|
72
|
+
part_number INTEGER NOT NULL,
|
|
73
|
+
etag TEXT NOT NULL,
|
|
74
|
+
size INTEGER NOT NULL,
|
|
75
|
+
file_path TEXT NOT NULL,
|
|
76
|
+
PRIMARY KEY (upload_id, part_number)
|
|
77
|
+
)
|
|
78
|
+
`);
|
|
79
|
+
|
|
80
|
+
db.run(`
|
|
81
|
+
CREATE TABLE IF NOT EXISTS do_instances (
|
|
82
|
+
namespace TEXT NOT NULL,
|
|
83
|
+
id TEXT NOT NULL,
|
|
84
|
+
name TEXT,
|
|
85
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
86
|
+
PRIMARY KEY (namespace, id)
|
|
87
|
+
)
|
|
88
|
+
`);
|
|
89
|
+
|
|
90
|
+
db.run(`
|
|
91
|
+
CREATE TABLE IF NOT EXISTS do_storage (
|
|
92
|
+
namespace TEXT NOT NULL,
|
|
93
|
+
id TEXT NOT NULL,
|
|
94
|
+
key TEXT NOT NULL,
|
|
95
|
+
value TEXT NOT NULL,
|
|
96
|
+
PRIMARY KEY (namespace, id, key)
|
|
97
|
+
)
|
|
98
|
+
`);
|
|
99
|
+
|
|
100
|
+
db.run(`
|
|
101
|
+
CREATE TABLE IF NOT EXISTS do_alarms (
|
|
102
|
+
namespace TEXT NOT NULL,
|
|
103
|
+
id TEXT NOT NULL,
|
|
104
|
+
alarm_time INTEGER NOT NULL,
|
|
105
|
+
PRIMARY KEY (namespace, id)
|
|
106
|
+
)
|
|
107
|
+
`);
|
|
108
|
+
|
|
109
|
+
db.run(`
|
|
110
|
+
CREATE TABLE IF NOT EXISTS queue_messages (
|
|
111
|
+
id TEXT PRIMARY KEY,
|
|
112
|
+
queue TEXT NOT NULL,
|
|
113
|
+
body BLOB NOT NULL,
|
|
114
|
+
content_type TEXT NOT NULL DEFAULT 'json',
|
|
115
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
116
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
117
|
+
visible_at INTEGER NOT NULL,
|
|
118
|
+
created_at INTEGER NOT NULL,
|
|
119
|
+
completed_at INTEGER
|
|
120
|
+
)
|
|
121
|
+
`);
|
|
122
|
+
|
|
123
|
+
// Migrate: add status column if missing (existing databases)
|
|
124
|
+
{
|
|
125
|
+
const cols = db.query<{ name: string }, []>("PRAGMA table_info(queue_messages)").all();
|
|
126
|
+
if (!cols.some(c => c.name === "status")) {
|
|
127
|
+
db.run("ALTER TABLE queue_messages ADD COLUMN status TEXT NOT NULL DEFAULT 'pending'");
|
|
128
|
+
}
|
|
129
|
+
if (!cols.some(c => c.name === "completed_at")) {
|
|
130
|
+
db.run("ALTER TABLE queue_messages ADD COLUMN completed_at INTEGER");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_queue_visible ON queue_messages(queue, visible_at)`);
|
|
135
|
+
|
|
136
|
+
db.run(`
|
|
137
|
+
CREATE TABLE IF NOT EXISTS queue_leases (
|
|
138
|
+
lease_id TEXT PRIMARY KEY,
|
|
139
|
+
message_id TEXT NOT NULL,
|
|
140
|
+
queue TEXT NOT NULL,
|
|
141
|
+
expires_at INTEGER NOT NULL
|
|
142
|
+
)
|
|
143
|
+
`);
|
|
144
|
+
|
|
145
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_queue_leases_queue ON queue_leases(queue)`);
|
|
146
|
+
|
|
147
|
+
db.run(`
|
|
148
|
+
CREATE TABLE IF NOT EXISTS workflow_instances (
|
|
149
|
+
id TEXT PRIMARY KEY,
|
|
150
|
+
workflow_name TEXT NOT NULL,
|
|
151
|
+
class_name TEXT NOT NULL,
|
|
152
|
+
params TEXT,
|
|
153
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
154
|
+
output TEXT,
|
|
155
|
+
error TEXT,
|
|
156
|
+
error_name TEXT,
|
|
157
|
+
created_at INTEGER NOT NULL,
|
|
158
|
+
updated_at INTEGER NOT NULL
|
|
159
|
+
)
|
|
160
|
+
`);
|
|
161
|
+
|
|
162
|
+
db.run(`
|
|
163
|
+
CREATE TABLE IF NOT EXISTS workflow_events (
|
|
164
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
165
|
+
instance_id TEXT NOT NULL,
|
|
166
|
+
event_type TEXT NOT NULL,
|
|
167
|
+
payload TEXT,
|
|
168
|
+
created_at INTEGER NOT NULL
|
|
169
|
+
)
|
|
170
|
+
`);
|
|
171
|
+
|
|
172
|
+
db.run(`
|
|
173
|
+
CREATE TABLE IF NOT EXISTS workflow_steps (
|
|
174
|
+
instance_id TEXT NOT NULL,
|
|
175
|
+
step_name TEXT NOT NULL,
|
|
176
|
+
output TEXT,
|
|
177
|
+
completed_at INTEGER NOT NULL,
|
|
178
|
+
PRIMARY KEY (instance_id, step_name)
|
|
179
|
+
)
|
|
180
|
+
`);
|
|
181
|
+
|
|
182
|
+
db.run(`
|
|
183
|
+
CREATE TABLE IF NOT EXISTS workflow_step_attempts (
|
|
184
|
+
instance_id TEXT NOT NULL,
|
|
185
|
+
step_name TEXT NOT NULL,
|
|
186
|
+
failed_attempts INTEGER NOT NULL DEFAULT 0,
|
|
187
|
+
last_error TEXT,
|
|
188
|
+
last_error_name TEXT,
|
|
189
|
+
last_error_id TEXT,
|
|
190
|
+
updated_at INTEGER,
|
|
191
|
+
PRIMARY KEY (instance_id, step_name)
|
|
192
|
+
)
|
|
193
|
+
`);
|
|
194
|
+
|
|
195
|
+
db.run(`
|
|
196
|
+
CREATE TABLE IF NOT EXISTS cache_entries (
|
|
197
|
+
cache_name TEXT NOT NULL,
|
|
198
|
+
url TEXT NOT NULL,
|
|
199
|
+
status INTEGER NOT NULL,
|
|
200
|
+
headers TEXT NOT NULL,
|
|
201
|
+
body BLOB NOT NULL,
|
|
202
|
+
expires_at INTEGER,
|
|
203
|
+
PRIMARY KEY (cache_name, url)
|
|
204
|
+
)
|
|
205
|
+
`);
|
|
206
|
+
|
|
207
|
+
db.run(`
|
|
208
|
+
CREATE TABLE IF NOT EXISTS email_messages (
|
|
209
|
+
id TEXT PRIMARY KEY,
|
|
210
|
+
binding TEXT NOT NULL,
|
|
211
|
+
from_addr TEXT NOT NULL,
|
|
212
|
+
to_addr TEXT NOT NULL,
|
|
213
|
+
raw BLOB NOT NULL,
|
|
214
|
+
raw_size INTEGER NOT NULL,
|
|
215
|
+
status TEXT NOT NULL DEFAULT 'sent',
|
|
216
|
+
reject_reason TEXT,
|
|
217
|
+
created_at INTEGER NOT NULL
|
|
218
|
+
)
|
|
219
|
+
`);
|
|
220
|
+
|
|
221
|
+
db.run(`
|
|
222
|
+
CREATE TABLE IF NOT EXISTS ai_requests (
|
|
223
|
+
id TEXT PRIMARY KEY,
|
|
224
|
+
model TEXT NOT NULL,
|
|
225
|
+
input_summary TEXT,
|
|
226
|
+
output_summary TEXT,
|
|
227
|
+
duration_ms INTEGER NOT NULL,
|
|
228
|
+
status TEXT NOT NULL DEFAULT 'ok',
|
|
229
|
+
error TEXT,
|
|
230
|
+
is_streaming INTEGER NOT NULL DEFAULT 0,
|
|
231
|
+
created_at INTEGER NOT NULL
|
|
232
|
+
)
|
|
233
|
+
`);
|
|
234
|
+
|
|
235
|
+
db.run(`
|
|
236
|
+
CREATE TABLE IF NOT EXISTS analytics_engine (
|
|
237
|
+
id TEXT PRIMARY KEY,
|
|
238
|
+
dataset TEXT NOT NULL,
|
|
239
|
+
timestamp INTEGER NOT NULL,
|
|
240
|
+
_sample_interval INTEGER NOT NULL DEFAULT 1,
|
|
241
|
+
index1 TEXT,
|
|
242
|
+
blob1 TEXT, blob2 TEXT, blob3 TEXT, blob4 TEXT, blob5 TEXT,
|
|
243
|
+
blob6 TEXT, blob7 TEXT, blob8 TEXT, blob9 TEXT, blob10 TEXT,
|
|
244
|
+
blob11 TEXT, blob12 TEXT, blob13 TEXT, blob14 TEXT, blob15 TEXT,
|
|
245
|
+
blob16 TEXT, blob17 TEXT, blob18 TEXT, blob19 TEXT, blob20 TEXT,
|
|
246
|
+
double1 REAL, double2 REAL, double3 REAL, double4 REAL, double5 REAL,
|
|
247
|
+
double6 REAL, double7 REAL, double8 REAL, double9 REAL, double10 REAL,
|
|
248
|
+
double11 REAL, double12 REAL, double13 REAL, double14 REAL, double15 REAL,
|
|
249
|
+
double16 REAL, double17 REAL, double18 REAL, double19 REAL, double20 REAL
|
|
250
|
+
)
|
|
251
|
+
`);
|
|
252
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_analytics_engine_dataset_ts ON analytics_engine(dataset, timestamp)`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Returns the path to the .bunflare data directory. */
|
|
256
|
+
export function getDataDir(): string {
|
|
257
|
+
return DATA_DIR;
|
|
258
|
+
}
|
package/runtime/env.ts
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import type { WranglerConfig } from "./config";
|
|
4
|
+
import { SqliteKVNamespace } from "./bindings/kv";
|
|
5
|
+
import { FileR2Bucket } from "./bindings/r2";
|
|
6
|
+
import { DurableObjectNamespaceImpl } from "./bindings/durable-object";
|
|
7
|
+
import { SqliteWorkflowBinding } from "./bindings/workflow";
|
|
8
|
+
import { openD1Database } from "./bindings/d1";
|
|
9
|
+
import { SqliteQueueProducer, QueueConsumer } from "./bindings/queue";
|
|
10
|
+
import { SendEmailBinding } from "./bindings/email";
|
|
11
|
+
import { HyperdriveBinding } from "./bindings/hyperdrive";
|
|
12
|
+
import { AiBinding } from "./bindings/ai";
|
|
13
|
+
import { createServiceBinding } from "./bindings/service-binding";
|
|
14
|
+
import { StaticAssets } from "./bindings/static-assets";
|
|
15
|
+
import { ImagesBinding } from "./bindings/images";
|
|
16
|
+
import { BrowserBinding } from "./bindings/browser";
|
|
17
|
+
import { SqliteAnalyticsEngine } from "./bindings/analytics-engine";
|
|
18
|
+
import { DockerManager } from "./bindings/container-docker";
|
|
19
|
+
import { ContainerBase } from "./bindings/container";
|
|
20
|
+
import type { DOExecutorFactory } from "./bindings/do-executor";
|
|
21
|
+
import type { WorkerRegistry } from "./worker-registry";
|
|
22
|
+
import { getDatabase, getDataDir } from "./db";
|
|
23
|
+
import { instrumentBinding, instrumentD1, instrumentDONamespace, instrumentServiceBinding } from "./tracing/instrument";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Global reference to the built env object. Used by cloudflare:workers `env` export.
|
|
27
|
+
* Must remain the same object reference — we mutate it in place so that
|
|
28
|
+
* `import { env } from "cloudflare:workers"` always sees current bindings.
|
|
29
|
+
*/
|
|
30
|
+
export const globalEnv: Record<string, unknown> = {};
|
|
31
|
+
|
|
32
|
+
export function setGlobalEnv(env: Record<string, unknown>) {
|
|
33
|
+
for (const key of Object.keys(globalEnv)) {
|
|
34
|
+
delete globalEnv[key];
|
|
35
|
+
}
|
|
36
|
+
Object.assign(globalEnv, env);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function parseDevVars(content: string): Record<string, string> {
|
|
40
|
+
const vars: Record<string, string> = {};
|
|
41
|
+
for (const line of content.split("\n")) {
|
|
42
|
+
const trimmed = line.trim();
|
|
43
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
44
|
+
const eqIndex = trimmed.indexOf("=");
|
|
45
|
+
if (eqIndex === -1) continue;
|
|
46
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
47
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
48
|
+
// Strip surrounding quotes
|
|
49
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
50
|
+
value = value.slice(1, -1);
|
|
51
|
+
}
|
|
52
|
+
vars[key] = value;
|
|
53
|
+
}
|
|
54
|
+
return vars;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface ConsumerConfig {
|
|
58
|
+
queue: string;
|
|
59
|
+
maxBatchSize: number;
|
|
60
|
+
maxBatchTimeout: number;
|
|
61
|
+
maxRetries: number;
|
|
62
|
+
deadLetterQueue: string | null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface ServiceBindingEntry {
|
|
66
|
+
bindingName: string;
|
|
67
|
+
serviceName: string;
|
|
68
|
+
entrypoint?: string;
|
|
69
|
+
proxy: Record<string, unknown>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface ClassRegistry {
|
|
73
|
+
durableObjects: { bindingName: string; className: string; namespace: DurableObjectNamespaceImpl }[];
|
|
74
|
+
workflows: { bindingName: string; className: string; binding: SqliteWorkflowBinding }[];
|
|
75
|
+
containers: { className: string; image: string; maxInstances?: number; namespace: DurableObjectNamespaceImpl }[];
|
|
76
|
+
queueConsumers: ConsumerConfig[];
|
|
77
|
+
serviceBindings: ServiceBindingEntry[];
|
|
78
|
+
staticAssets: StaticAssets | null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function buildEnv(config: WranglerConfig, devVarsDir?: string, executorFactory?: DOExecutorFactory, browserConfig?: { wsEndpoint?: string; executablePath?: string; headless?: boolean }): { env: Record<string, unknown>; registry: ClassRegistry } {
|
|
82
|
+
const env: Record<string, unknown> = {};
|
|
83
|
+
const registry: ClassRegistry = { durableObjects: [], workflows: [], containers: [], queueConsumers: [], serviceBindings: [], staticAssets: null };
|
|
84
|
+
|
|
85
|
+
// Environment variables from config
|
|
86
|
+
if (config.vars) {
|
|
87
|
+
for (const [key, value] of Object.entries(config.vars)) {
|
|
88
|
+
env[key] = value;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Override with .dev.vars or .env file (if exists)
|
|
93
|
+
// .dev.vars takes priority over .env (matching CF behavior)
|
|
94
|
+
if (devVarsDir) {
|
|
95
|
+
const devVarsPath = path.join(devVarsDir, ".dev.vars");
|
|
96
|
+
const envPath = path.join(devVarsDir, ".env");
|
|
97
|
+
const filePath = existsSync(devVarsPath) ? devVarsPath : existsSync(envPath) ? envPath : null;
|
|
98
|
+
if (filePath) {
|
|
99
|
+
const content = readFileSync(filePath, "utf-8");
|
|
100
|
+
const devVars = parseDevVars(content);
|
|
101
|
+
for (const [key, value] of Object.entries(devVars)) {
|
|
102
|
+
env[key] = value;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// KV namespaces
|
|
108
|
+
const db = getDatabase();
|
|
109
|
+
for (const kv of config.kv_namespaces ?? []) {
|
|
110
|
+
console.log(`[bunflare] KV namespace: ${kv.binding}`);
|
|
111
|
+
env[kv.binding] = instrumentBinding(new SqliteKVNamespace(db, kv.id), {
|
|
112
|
+
type: "kv", name: kv.binding,
|
|
113
|
+
methods: ["get", "getWithMetadata", "put", "delete", "list"],
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// R2 buckets
|
|
118
|
+
for (const r2 of config.r2_buckets ?? []) {
|
|
119
|
+
console.log(`[bunflare] R2 bucket: ${r2.binding} (${r2.bucket_name})`);
|
|
120
|
+
env[r2.binding] = instrumentBinding(new FileR2Bucket(db, r2.bucket_name, getDataDir()), {
|
|
121
|
+
type: "r2", name: r2.binding,
|
|
122
|
+
methods: ["get", "put", "delete", "list", "head", "createMultipartUpload"],
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Durable Objects
|
|
127
|
+
for (const doBinding of config.durable_objects?.bindings ?? []) {
|
|
128
|
+
console.log(`[bunflare] Durable Object: ${doBinding.name} -> ${doBinding.class_name}`);
|
|
129
|
+
const namespace = new DurableObjectNamespaceImpl(db, doBinding.class_name, getDataDir(), undefined, executorFactory);
|
|
130
|
+
env[doBinding.name] = instrumentDONamespace(namespace, doBinding.class_name);
|
|
131
|
+
registry.durableObjects.push({
|
|
132
|
+
bindingName: doBinding.name,
|
|
133
|
+
className: doBinding.class_name,
|
|
134
|
+
namespace,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Workflows
|
|
139
|
+
for (const wf of config.workflows ?? []) {
|
|
140
|
+
console.log(`[bunflare] Workflow: ${wf.binding} -> ${wf.class_name}`);
|
|
141
|
+
const binding = new SqliteWorkflowBinding(db, wf.binding, wf.class_name, wf.limits);
|
|
142
|
+
env[wf.binding] = instrumentBinding(binding, {
|
|
143
|
+
type: "workflow", name: wf.binding,
|
|
144
|
+
methods: ["create", "get"],
|
|
145
|
+
});
|
|
146
|
+
registry.workflows.push({
|
|
147
|
+
bindingName: wf.binding,
|
|
148
|
+
className: wf.class_name,
|
|
149
|
+
binding,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// D1 databases
|
|
154
|
+
for (const d1 of config.d1_databases ?? []) {
|
|
155
|
+
console.log(`[bunflare] D1 database: ${d1.binding} (${d1.database_name})`);
|
|
156
|
+
env[d1.binding] = instrumentD1(openD1Database(getDataDir(), d1.database_name), d1.binding);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Queue producers
|
|
160
|
+
for (const producer of config.queues?.producers ?? []) {
|
|
161
|
+
console.log(`[bunflare] Queue producer: ${producer.binding} -> ${producer.queue}`);
|
|
162
|
+
env[producer.binding] = instrumentBinding(new SqliteQueueProducer(db, producer.queue, producer.delivery_delay ?? 0), {
|
|
163
|
+
type: "queue", name: producer.binding,
|
|
164
|
+
methods: ["send", "sendBatch"],
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Queue consumers (configs — actual consumers started in dev.ts after worker import)
|
|
169
|
+
for (const consumer of config.queues?.consumers ?? []) {
|
|
170
|
+
console.log(`[bunflare] Queue consumer: ${consumer.queue}`);
|
|
171
|
+
registry.queueConsumers.push({
|
|
172
|
+
queue: consumer.queue,
|
|
173
|
+
maxBatchSize: consumer.max_batch_size ?? 10,
|
|
174
|
+
maxBatchTimeout: consumer.max_batch_timeout ?? 5,
|
|
175
|
+
maxRetries: consumer.max_retries ?? 3,
|
|
176
|
+
deadLetterQueue: consumer.dead_letter_queue ?? null,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Service bindings
|
|
181
|
+
for (const svc of config.services ?? []) {
|
|
182
|
+
console.log(`[bunflare] Service binding: ${svc.binding} -> ${svc.service}${svc.entrypoint ? ` (${svc.entrypoint})` : ""}`);
|
|
183
|
+
const proxy = createServiceBinding(svc.service, svc.entrypoint);
|
|
184
|
+
env[svc.binding] = instrumentServiceBinding(proxy as object, svc.service) as Record<string, unknown>;
|
|
185
|
+
registry.serviceBindings.push({
|
|
186
|
+
bindingName: svc.binding,
|
|
187
|
+
serviceName: svc.service,
|
|
188
|
+
entrypoint: svc.entrypoint,
|
|
189
|
+
proxy,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Images binding
|
|
194
|
+
if (config.images) {
|
|
195
|
+
console.log(`[bunflare] Images binding: ${config.images.binding}`);
|
|
196
|
+
env[config.images.binding] = instrumentBinding(new ImagesBinding(), {
|
|
197
|
+
type: "images", name: config.images.binding,
|
|
198
|
+
methods: ["info"],
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Send email bindings
|
|
203
|
+
for (const email of config.send_email ?? []) {
|
|
204
|
+
console.log(`[bunflare] Send email binding: ${email.name}`);
|
|
205
|
+
env[email.name] = instrumentBinding(
|
|
206
|
+
new SendEmailBinding(db, email.name, email.destination_address, email.allowed_destination_addresses),
|
|
207
|
+
{ type: "email", name: email.name, methods: ["send"] },
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Hyperdrive
|
|
212
|
+
for (const hd of config.hyperdrive ?? []) {
|
|
213
|
+
const connStr = hd.localConnectionString ?? "";
|
|
214
|
+
console.log(`[bunflare] Hyperdrive: ${hd.binding}`);
|
|
215
|
+
env[hd.binding] = new HyperdriveBinding(connStr);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Workers AI
|
|
219
|
+
if (config.ai) {
|
|
220
|
+
const accountId = (env.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID) as string | undefined;
|
|
221
|
+
const apiToken = (env.CLOUDFLARE_API_TOKEN ?? process.env.CLOUDFLARE_API_TOKEN) as string | undefined;
|
|
222
|
+
console.log(`[bunflare] AI binding: ${config.ai.binding}`);
|
|
223
|
+
env[config.ai.binding] = instrumentBinding(
|
|
224
|
+
new AiBinding(db, accountId, apiToken),
|
|
225
|
+
{ type: "ai", name: config.ai.binding, methods: ["run", "models"] },
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Analytics Engine datasets
|
|
230
|
+
for (const ae of config.analytics_engine_datasets ?? []) {
|
|
231
|
+
console.log(`[bunflare] Analytics Engine: ${ae.binding} (dataset: ${ae.dataset ?? ae.binding})`);
|
|
232
|
+
env[ae.binding] = instrumentBinding(
|
|
233
|
+
new SqliteAnalyticsEngine(db, ae.dataset ?? ae.binding),
|
|
234
|
+
{ type: "analytics_engine", name: ae.binding, methods: ["writeDataPoint"] },
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Containers — create DO namespaces for container classes
|
|
239
|
+
const doClassNames = new Set((config.durable_objects?.bindings ?? []).map(b => b.class_name));
|
|
240
|
+
for (const container of config.containers ?? []) {
|
|
241
|
+
// Skip if this class is already defined as a DO binding (avoid double-creating)
|
|
242
|
+
if (doClassNames.has(container.class_name)) {
|
|
243
|
+
// Find the existing namespace and register container config on it
|
|
244
|
+
const existing = registry.durableObjects.find(d => d.className === container.class_name);
|
|
245
|
+
if (existing) {
|
|
246
|
+
registry.containers.push({
|
|
247
|
+
className: container.class_name,
|
|
248
|
+
image: container.image,
|
|
249
|
+
maxInstances: container.max_instances,
|
|
250
|
+
namespace: existing.namespace,
|
|
251
|
+
});
|
|
252
|
+
console.log(`[bunflare] Container: ${container.class_name} (reusing DO binding, image: ${container.image})`);
|
|
253
|
+
}
|
|
254
|
+
} else {
|
|
255
|
+
// Create a new DO namespace for this container
|
|
256
|
+
const bindingName = container.name ?? container.class_name;
|
|
257
|
+
console.log(`[bunflare] Container: ${bindingName} -> ${container.class_name} (image: ${container.image})`);
|
|
258
|
+
const namespace = new DurableObjectNamespaceImpl(db, container.class_name, getDataDir(), undefined, executorFactory);
|
|
259
|
+
env[bindingName] = instrumentDONamespace(namespace, container.class_name);
|
|
260
|
+
registry.durableObjects.push({
|
|
261
|
+
bindingName,
|
|
262
|
+
className: container.class_name,
|
|
263
|
+
namespace,
|
|
264
|
+
});
|
|
265
|
+
registry.containers.push({
|
|
266
|
+
className: container.class_name,
|
|
267
|
+
image: container.image,
|
|
268
|
+
maxInstances: container.max_instances,
|
|
269
|
+
namespace,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Static assets
|
|
275
|
+
if (config.assets) {
|
|
276
|
+
const assetsDir = path.resolve(config.assets.directory);
|
|
277
|
+
const assets = new StaticAssets(assetsDir, config.assets.html_handling, config.assets.not_found_handling);
|
|
278
|
+
registry.staticAssets = assets;
|
|
279
|
+
if (config.assets.binding) {
|
|
280
|
+
console.log(`[bunflare] Static assets: ${config.assets.binding} -> ${config.assets.directory}`);
|
|
281
|
+
env[config.assets.binding] = instrumentBinding(assets, {
|
|
282
|
+
type: "assets", name: config.assets.binding,
|
|
283
|
+
methods: ["fetch"],
|
|
284
|
+
});
|
|
285
|
+
} else {
|
|
286
|
+
console.log(`[bunflare] Static assets: ${config.assets.directory} (auto-serve)`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Browser Rendering binding
|
|
291
|
+
if (config.browser) {
|
|
292
|
+
console.log(`[bunflare] Browser binding: ${config.browser.binding}`);
|
|
293
|
+
env[config.browser.binding] = instrumentBinding(
|
|
294
|
+
new BrowserBinding(browserConfig ?? {}),
|
|
295
|
+
{ type: "browser", name: config.browser.binding, methods: ["launch", "connect", "sessions"] },
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Version metadata binding
|
|
300
|
+
if (config.version_metadata) {
|
|
301
|
+
const binding = config.version_metadata.binding;
|
|
302
|
+
env[binding] = {
|
|
303
|
+
id: "local-dev",
|
|
304
|
+
tag: "",
|
|
305
|
+
timestamp: new Date().toISOString(),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Store reference for cloudflare:workers env export
|
|
310
|
+
setGlobalEnv(env);
|
|
311
|
+
|
|
312
|
+
return { env, registry };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function wireClassRefs(
|
|
316
|
+
registry: ClassRegistry,
|
|
317
|
+
workerModule: Record<string, unknown>,
|
|
318
|
+
env: Record<string, unknown>,
|
|
319
|
+
workerRegistry?: WorkerRegistry,
|
|
320
|
+
) {
|
|
321
|
+
for (const entry of registry.durableObjects) {
|
|
322
|
+
const cls = workerModule[entry.className];
|
|
323
|
+
if (!cls) throw new Error(`Durable Object class "${entry.className}" not exported from worker module`);
|
|
324
|
+
entry.namespace._setClass(cls as any, env);
|
|
325
|
+
console.log(`[bunflare] Wired DO class: ${entry.className}`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
for (const entry of registry.workflows) {
|
|
329
|
+
const cls = workerModule[entry.className];
|
|
330
|
+
if (!cls) throw new Error(`Workflow class "${entry.className}" not exported from worker module`);
|
|
331
|
+
entry.binding._setClass(cls as any, env);
|
|
332
|
+
entry.binding.resumeInterrupted();
|
|
333
|
+
console.log(`[bunflare] Wired Workflow class: ${entry.className}`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Wire container configs onto namespaces
|
|
337
|
+
const dockerManager = new DockerManager();
|
|
338
|
+
for (const entry of registry.containers) {
|
|
339
|
+
entry.namespace._setContainerConfig({
|
|
340
|
+
className: entry.className,
|
|
341
|
+
image: entry.image,
|
|
342
|
+
maxInstances: entry.maxInstances,
|
|
343
|
+
dockerManager,
|
|
344
|
+
});
|
|
345
|
+
console.log(`[bunflare] Wired container config: ${entry.className} (image: ${entry.image})`);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Wire service bindings
|
|
349
|
+
for (const entry of registry.serviceBindings) {
|
|
350
|
+
const wire = entry.proxy._wire as ((resolver: () => { workerModule: Record<string, unknown>; env: Record<string, unknown> }) => void) | undefined;
|
|
351
|
+
if (wire) {
|
|
352
|
+
if (workerRegistry) {
|
|
353
|
+
// Resolve through registry (handles both self-ref and cross-worker)
|
|
354
|
+
wire(() => workerRegistry.resolveTarget(entry.serviceName));
|
|
355
|
+
} else {
|
|
356
|
+
// Backward compat: self-reference
|
|
357
|
+
wire(() => ({ workerModule, env }));
|
|
358
|
+
}
|
|
359
|
+
console.log(`[bunflare] Wired service binding: ${entry.bindingName} -> ${entry.serviceName}${entry.entrypoint ? ` (${entry.entrypoint})` : ""}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|