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
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import { randomUUIDv7 } from "bun";
|
|
3
|
+
import { startSpan, setSpanAttribute } from "../tracing/span";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* EmailMessage — exported from `cloudflare:email`.
|
|
7
|
+
* Used to construct an email for sending via a send_email binding.
|
|
8
|
+
*/
|
|
9
|
+
export class EmailMessage {
|
|
10
|
+
readonly from: string;
|
|
11
|
+
readonly to: string;
|
|
12
|
+
readonly raw: ReadableStream<Uint8Array> | Uint8Array | ArrayBuffer | string;
|
|
13
|
+
|
|
14
|
+
constructor(from: string, to: string, raw: ReadableStream<Uint8Array> | Uint8Array | ArrayBuffer | string) {
|
|
15
|
+
this.from = from;
|
|
16
|
+
this.to = to;
|
|
17
|
+
this.raw = raw;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* SendEmailBinding — the `send_email` binding.
|
|
23
|
+
* Persists sent emails to SQLite.
|
|
24
|
+
*/
|
|
25
|
+
export class SendEmailBinding {
|
|
26
|
+
private db: Database;
|
|
27
|
+
private bindingName: string;
|
|
28
|
+
private destinationAddress: string | undefined;
|
|
29
|
+
private allowedDestinationAddresses: string[] | undefined;
|
|
30
|
+
|
|
31
|
+
constructor(db: Database, bindingName: string, destinationAddress?: string, allowedDestinationAddresses?: string[]) {
|
|
32
|
+
this.db = db;
|
|
33
|
+
this.bindingName = bindingName;
|
|
34
|
+
this.destinationAddress = destinationAddress;
|
|
35
|
+
this.allowedDestinationAddresses = allowedDestinationAddresses;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async send(message: EmailMessage): Promise<void> {
|
|
39
|
+
// Validate destination
|
|
40
|
+
if (this.destinationAddress && message.to !== this.destinationAddress) {
|
|
41
|
+
throw new Error(`Destination address "${message.to}" not allowed. Binding "${this.bindingName}" only allows sending to "${this.destinationAddress}".`);
|
|
42
|
+
}
|
|
43
|
+
if (this.allowedDestinationAddresses && this.allowedDestinationAddresses.length > 0) {
|
|
44
|
+
if (!this.allowedDestinationAddresses.includes(message.to)) {
|
|
45
|
+
throw new Error(`Destination address "${message.to}" not in allowed list for binding "${this.bindingName}".`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const rawBytes = await resolveRaw(message.raw);
|
|
50
|
+
const id = randomUUIDv7();
|
|
51
|
+
this.db.run(
|
|
52
|
+
"INSERT INTO email_messages (id, binding, from_addr, to_addr, raw, raw_size, status, created_at) VALUES (?, ?, ?, ?, ?, ?, 'sent', ?)",
|
|
53
|
+
[id, this.bindingName, message.from, message.to, rawBytes, rawBytes.byteLength, Date.now()],
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* ForwardableEmailMessage — the object passed to the worker's `email()` handler.
|
|
60
|
+
* Provides from, to, headers, raw, rawSize, setReject(), forward(), reply().
|
|
61
|
+
*/
|
|
62
|
+
export class ForwardableEmailMessage {
|
|
63
|
+
readonly from: string;
|
|
64
|
+
readonly to: string;
|
|
65
|
+
readonly headers: Headers;
|
|
66
|
+
readonly raw: ReadableStream<Uint8Array>;
|
|
67
|
+
readonly rawSize: number;
|
|
68
|
+
|
|
69
|
+
private db: Database;
|
|
70
|
+
private rawBytes: Uint8Array;
|
|
71
|
+
private messageId: string;
|
|
72
|
+
private rejected = false;
|
|
73
|
+
|
|
74
|
+
constructor(db: Database, messageId: string, from: string, to: string, rawBytes: Uint8Array) {
|
|
75
|
+
this.db = db;
|
|
76
|
+
this.messageId = messageId;
|
|
77
|
+
this.from = from;
|
|
78
|
+
this.to = to;
|
|
79
|
+
this.rawBytes = rawBytes;
|
|
80
|
+
this.rawSize = rawBytes.byteLength;
|
|
81
|
+
this.headers = parseEmailHeaders(rawBytes);
|
|
82
|
+
this.raw = new ReadableStream({
|
|
83
|
+
start(controller) {
|
|
84
|
+
controller.enqueue(rawBytes);
|
|
85
|
+
controller.close();
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
setReject(reason: string): void {
|
|
91
|
+
startSpan({ name: "email.setReject", kind: "client", attributes: { "email.reject_reason": reason } }, () => {
|
|
92
|
+
this.rejected = true;
|
|
93
|
+
this.db.run(
|
|
94
|
+
"UPDATE email_messages SET status = 'rejected', reject_reason = ? WHERE id = ?",
|
|
95
|
+
[reason, this.messageId],
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async forward(rcptTo: string, headers?: Headers): Promise<void> {
|
|
101
|
+
return startSpan({ name: "email.forward", kind: "client", attributes: { "email.forward_to": rcptTo } }, async () => {
|
|
102
|
+
const id = randomUUIDv7();
|
|
103
|
+
this.db.run(
|
|
104
|
+
"INSERT INTO email_messages (id, binding, from_addr, to_addr, raw, raw_size, status, created_at) VALUES (?, ?, ?, ?, ?, ?, 'forwarded', ?)",
|
|
105
|
+
[id, "_forward", this.from, rcptTo, this.rawBytes, this.rawBytes.byteLength, Date.now()],
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async reply(message: EmailMessage): Promise<void> {
|
|
111
|
+
return startSpan({ name: "email.reply", kind: "client", attributes: { "email.reply_to": message.to } }, async () => {
|
|
112
|
+
const rawBytes = await resolveRaw(message.raw);
|
|
113
|
+
const id = randomUUIDv7();
|
|
114
|
+
this.db.run(
|
|
115
|
+
"INSERT INTO email_messages (id, binding, from_addr, to_addr, raw, raw_size, status, created_at) VALUES (?, ?, ?, ?, ?, ?, 'sent', ?)",
|
|
116
|
+
[id, "_reply", message.from, message.to, rawBytes, rawBytes.byteLength, Date.now()],
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─── Helpers ──────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
async function resolveRaw(raw: ReadableStream<Uint8Array> | Uint8Array | ArrayBuffer | string): Promise<Uint8Array> {
|
|
125
|
+
if (typeof raw === "string") return new TextEncoder().encode(raw);
|
|
126
|
+
if (raw instanceof Uint8Array) return raw;
|
|
127
|
+
if (raw instanceof ArrayBuffer) return new Uint8Array(raw);
|
|
128
|
+
// ReadableStream
|
|
129
|
+
const reader = raw.getReader();
|
|
130
|
+
const chunks: Uint8Array[] = [];
|
|
131
|
+
while (true) {
|
|
132
|
+
const { done, value } = await reader.read();
|
|
133
|
+
if (done) break;
|
|
134
|
+
chunks.push(value);
|
|
135
|
+
}
|
|
136
|
+
const totalLen = chunks.reduce((s, c) => s + c.byteLength, 0);
|
|
137
|
+
const result = new Uint8Array(totalLen);
|
|
138
|
+
let offset = 0;
|
|
139
|
+
for (const chunk of chunks) {
|
|
140
|
+
result.set(chunk, offset);
|
|
141
|
+
offset += chunk.byteLength;
|
|
142
|
+
}
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function parseEmailHeaders(rawBytes: Uint8Array): Headers {
|
|
147
|
+
const headers = new Headers();
|
|
148
|
+
const text = new TextDecoder().decode(rawBytes);
|
|
149
|
+
// Headers end at the first blank line
|
|
150
|
+
const headerEnd = text.indexOf("\r\n\r\n");
|
|
151
|
+
const headerSection = headerEnd !== -1 ? text.slice(0, headerEnd) : text.indexOf("\n\n") !== -1 ? text.slice(0, text.indexOf("\n\n")) : text;
|
|
152
|
+
|
|
153
|
+
const lines = headerSection.split(/\r?\n/);
|
|
154
|
+
let currentKey = "";
|
|
155
|
+
let currentValue = "";
|
|
156
|
+
|
|
157
|
+
for (const line of lines) {
|
|
158
|
+
if (/^\s/.test(line) && currentKey) {
|
|
159
|
+
// Continuation line
|
|
160
|
+
currentValue += " " + line.trim();
|
|
161
|
+
} else {
|
|
162
|
+
if (currentKey) {
|
|
163
|
+
headers.append(currentKey, currentValue);
|
|
164
|
+
}
|
|
165
|
+
const colonIdx = line.indexOf(":");
|
|
166
|
+
if (colonIdx !== -1) {
|
|
167
|
+
currentKey = line.slice(0, colonIdx).trim();
|
|
168
|
+
currentValue = line.slice(colonIdx + 1).trim();
|
|
169
|
+
} else {
|
|
170
|
+
currentKey = "";
|
|
171
|
+
currentValue = "";
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (currentKey) {
|
|
176
|
+
headers.append(currentKey, currentValue);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return headers;
|
|
180
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HTMLRewriter as RawHTMLRewriter,
|
|
3
|
+
type ElementHandlers,
|
|
4
|
+
type DocumentHandlers,
|
|
5
|
+
} from "html-rewriter-wasm";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Cloudflare-compatible HTMLRewriter that wraps html-rewriter-wasm.
|
|
9
|
+
* Usage: new HTMLRewriter().on(selector, handler).onDocument(handler).transform(response)
|
|
10
|
+
*/
|
|
11
|
+
export class HTMLRewriter {
|
|
12
|
+
private elementHandlers: Array<[string, ElementHandlers]> = [];
|
|
13
|
+
private documentHandlers: DocumentHandlers[] = [];
|
|
14
|
+
|
|
15
|
+
on(selector: string, handler: ElementHandlers): this {
|
|
16
|
+
this.elementHandlers.push([selector, handler]);
|
|
17
|
+
return this;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
onDocument(handler: DocumentHandlers): this {
|
|
21
|
+
this.documentHandlers.push(handler);
|
|
22
|
+
return this;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
transform(response: Response): Response {
|
|
26
|
+
// If body is null, return as-is (e.g. 204 No Content)
|
|
27
|
+
if (response.body === null) {
|
|
28
|
+
return new Response(null, {
|
|
29
|
+
status: response.status,
|
|
30
|
+
statusText: response.statusText,
|
|
31
|
+
headers: new Headers(response.headers),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const elementHandlers = this.elementHandlers;
|
|
36
|
+
const documentHandlers = this.documentHandlers;
|
|
37
|
+
|
|
38
|
+
const { readable, writable } = new TransformStream<Uint8Array, Uint8Array>();
|
|
39
|
+
const writer = writable.getWriter();
|
|
40
|
+
|
|
41
|
+
const rewriter = new RawHTMLRewriter((chunk: Uint8Array) => {
|
|
42
|
+
writer.write(chunk);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
for (const [selector, handler] of elementHandlers) {
|
|
46
|
+
rewriter.on(selector, handler);
|
|
47
|
+
}
|
|
48
|
+
for (const handler of documentHandlers) {
|
|
49
|
+
rewriter.onDocument(handler);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const encoder = new TextEncoder();
|
|
53
|
+
const reader = response.body.getReader();
|
|
54
|
+
|
|
55
|
+
(async () => {
|
|
56
|
+
try {
|
|
57
|
+
while (true) {
|
|
58
|
+
const { done, value } = await reader.read();
|
|
59
|
+
if (done) break;
|
|
60
|
+
// value can be Uint8Array or string
|
|
61
|
+
const chunk = typeof value === "string" ? encoder.encode(value) : value;
|
|
62
|
+
await rewriter.write(chunk);
|
|
63
|
+
}
|
|
64
|
+
await rewriter.end();
|
|
65
|
+
} catch (err) {
|
|
66
|
+
await writer.abort(err instanceof Error ? err : new Error(String(err)));
|
|
67
|
+
return;
|
|
68
|
+
} finally {
|
|
69
|
+
rewriter.free();
|
|
70
|
+
}
|
|
71
|
+
await writer.close();
|
|
72
|
+
})();
|
|
73
|
+
|
|
74
|
+
// Copy headers but remove content-length since streaming transform changes size
|
|
75
|
+
const headers = new Headers(response.headers);
|
|
76
|
+
headers.delete("content-length");
|
|
77
|
+
|
|
78
|
+
return new Response(readable, {
|
|
79
|
+
status: response.status,
|
|
80
|
+
statusText: response.statusText,
|
|
81
|
+
headers,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local implementation of the Cloudflare Hyperdrive binding.
|
|
3
|
+
* Parses a PostgreSQL connection string and exposes readonly properties.
|
|
4
|
+
* connect() creates a raw TCP socket via Bun.connect().
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
interface HyperdriveSocket {
|
|
8
|
+
readable: ReadableStream<Uint8Array>;
|
|
9
|
+
writable: WritableStream<Uint8Array>;
|
|
10
|
+
closed: Promise<void>;
|
|
11
|
+
opened: Promise<{ remoteAddress: string }>;
|
|
12
|
+
close(): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class HyperdriveBinding {
|
|
16
|
+
private readonly _url: URL | null;
|
|
17
|
+
private readonly _connectionString: string;
|
|
18
|
+
|
|
19
|
+
constructor(connectionString: string) {
|
|
20
|
+
this._connectionString = connectionString;
|
|
21
|
+
if (connectionString) {
|
|
22
|
+
this._url = new URL(connectionString);
|
|
23
|
+
} else {
|
|
24
|
+
this._url = null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get connectionString(): string {
|
|
29
|
+
return this._connectionString;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get host(): string {
|
|
33
|
+
return this._url?.hostname ?? "";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get port(): number {
|
|
37
|
+
if (!this._url) return 5432;
|
|
38
|
+
return this._url.port ? parseInt(this._url.port, 10) : 5432;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get user(): string {
|
|
42
|
+
return this._url ? decodeURIComponent(this._url.username) : "";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get password(): string {
|
|
46
|
+
return this._url ? decodeURIComponent(this._url.password) : "";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get database(): string {
|
|
50
|
+
if (!this._url) return "";
|
|
51
|
+
// pathname is "/<database>", strip leading slash
|
|
52
|
+
return decodeURIComponent(this._url.pathname.slice(1));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
connect(): HyperdriveSocket {
|
|
56
|
+
if (!this._url) {
|
|
57
|
+
throw new Error("Hyperdrive: no connection string configured");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const host = this.host;
|
|
61
|
+
const port = this.port;
|
|
62
|
+
|
|
63
|
+
let readableController: ReadableStreamDefaultController<Uint8Array>;
|
|
64
|
+
let resolveOpened: (info: { remoteAddress: string }) => void;
|
|
65
|
+
let resolveClosed: () => void;
|
|
66
|
+
let rejectOpened: (err: Error) => void;
|
|
67
|
+
let rejectClosed: (err: Error) => void;
|
|
68
|
+
let bunSocket: ReturnType<typeof Bun.connect> extends Promise<infer T> ? T : never;
|
|
69
|
+
|
|
70
|
+
const readable = new ReadableStream<Uint8Array>({
|
|
71
|
+
start(controller) {
|
|
72
|
+
readableController = controller;
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const opened = new Promise<{ remoteAddress: string }>((resolve, reject) => {
|
|
77
|
+
resolveOpened = resolve;
|
|
78
|
+
rejectOpened = reject;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const closed = new Promise<void>((resolve, reject) => {
|
|
82
|
+
resolveClosed = resolve;
|
|
83
|
+
rejectClosed = reject;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const writable = new WritableStream<Uint8Array>({
|
|
87
|
+
async write(chunk) {
|
|
88
|
+
const sock = await socketPromise;
|
|
89
|
+
sock.write(chunk);
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const socketPromise = Bun.connect({
|
|
94
|
+
hostname: host,
|
|
95
|
+
port,
|
|
96
|
+
socket: {
|
|
97
|
+
data(_socket, data) {
|
|
98
|
+
readableController.enqueue(new Uint8Array(data));
|
|
99
|
+
},
|
|
100
|
+
open(socket) {
|
|
101
|
+
bunSocket = socket as any;
|
|
102
|
+
resolveOpened!({ remoteAddress: `${host}:${port}` });
|
|
103
|
+
},
|
|
104
|
+
close() {
|
|
105
|
+
try { readableController.close(); } catch {}
|
|
106
|
+
resolveClosed!();
|
|
107
|
+
},
|
|
108
|
+
error(_socket, err) {
|
|
109
|
+
rejectOpened!(err);
|
|
110
|
+
rejectClosed!(err);
|
|
111
|
+
try { readableController.error(err); } catch {}
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
readable,
|
|
118
|
+
writable,
|
|
119
|
+
opened,
|
|
120
|
+
closed,
|
|
121
|
+
close() {
|
|
122
|
+
socketPromise.then(s => s.end());
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
startTls(): never {
|
|
128
|
+
throw new Error("startTls() is not supported in local dev mode");
|
|
129
|
+
}
|
|
130
|
+
}
|