Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/hyperdrive-remote-bindings-local-dev.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"@cloudflare/config": minor
"miniflare": minor
"wrangler": minor
"@cloudflare/vite-plugin": minor
"@cloudflare/workers-utils": minor
---

Support remote Hyperdrive bindings in local development

```jsonc
{
"hyperdrive": [
{
"binding": "HYPERDRIVE",
"id": "<your-hyperdrive-id>",
"remote": true,
},
],
}
```
17 changes: 17 additions & 0 deletions packages/config/src/__tests__/convert.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,23 @@ describe("convertToWranglerConfig", () => {
]);
});

it("maps remote Hyperdrive without a local connection string", ({
expect,
}) => {
const result = convertToWranglerConfig({
worker: {
...baseWorker,
env: {
HD: { type: "hyperdrive", id: "h-1", dev: { remote: true } },
},
},
containers: [],
});
expect(result.hyperdrive).toEqual([
{ binding: "HD", id: "h-1", remote: true },
]);
});

it("maps pipeline.name to stream", ({ expect }) => {
const result = convertToWranglerConfig({
worker: {
Expand Down
2 changes: 1 addition & 1 deletion packages/config/src/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ export interface HyperdriveBindingOptions {
/** The ID of the Hyperdrive configuration. */
id: string;
/** Options that only apply during local development. */
dev?: {
dev?: BindingDevOptions & {
/** The database connection string used during local development. */
connectionString?: string;
};
Expand Down
1 change: 1 addition & 0 deletions packages/config/src/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,7 @@ function convertBindingsAndAssets(
omitUndefined({
binding: name,
id: binding.id,
remote: binding.dev?.remote,
localConnectionString: binding.dev?.connectionString,
})
);
Expand Down
4 changes: 3 additions & 1 deletion packages/config/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@ export const FlagshipBindingSchema = z.strictObject({
export const HyperdriveBindingSchema = z.strictObject({
type: z.literal("hyperdrive"),
id: z.string(),
dev: z.strictObject({ connectionString: z.string().optional() }).optional(),
dev: RemoteBindingDevSchema.extend({
connectionString: z.string().optional(),
}).optional(),
});

export const KnownBindingSchema = z.discriminatedUnion("type", [
Expand Down
17 changes: 16 additions & 1 deletion packages/miniflare/src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,23 @@ const MiniflareBrowserBindingSchema = BrowserBindingSchema.extend({
headful: z.boolean().optional(),
});

/**
* Remote Hyperdrive bindings get their connection string from the proxy session.
*/
const MiniflareHyperdriveBindingSchema = HyperdriveBindingSchema.extend({
dev: z.strictObject({ connectionString: z.string() }),
dev: z.strictObject({
connectionString: z.string().optional(),
remote: z.boolean().optional(),
}),
}).superRefine((value, ctx) => {
if (!value.dev.remote && value.dev.connectionString === undefined) {
ctx.addIssue({
code: "custom",
path: ["dev", "connectionString"],
message:
"A Hyperdrive binding requires a connection string unless it sets 'remote': true.",
});
}
});

/**
Expand Down
7 changes: 5 additions & 2 deletions packages/miniflare/src/config/v4-convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -704,10 +704,13 @@ function addProductBindings(
env[name] = { type: "analytics-engine-dataset", name: binding.dataset };
}
for (const [name, value] of Object.entries(worker.hyperdrives ?? {})) {
const isRemoteEntry = typeof value === "object" && !(value instanceof URL);
env[name] = {
type: "hyperdrive",
id: name,
dev: { connectionString: String(value) },
id: isRemoteEntry && value.id ? value.id : name,
dev: isRemoteEntry
? { remote: isRemote(value.remoteProxyConnectionString) }
: { connectionString: String(value) },
};
}
for (const [name, binding] of Object.entries(worker.ratelimits ?? {})) {
Expand Down
20 changes: 18 additions & 2 deletions packages/miniflare/src/config/v4-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ const V4QueueConsumerOptionsSchema = z.object({
retryDelay: V4QueueMessageDelaySchema,
});

const V4HyperdriveSchema = z
const V4HyperdriveConnectionSchema = z
.union([z.url(), z.instanceof(URL)])
.superRefine((value, ctx) => {
const url = typeof value === "string" ? new URL(value) : value;
Expand Down Expand Up @@ -323,6 +323,14 @@ const V4HyperdriveSchema = z
}
});

const V4HyperdriveSchema = z.union([
V4HyperdriveConnectionSchema,
z.object({
id: z.string().optional(),
remoteProxyConnectionString: RemoteProxyConnectionStringSchema,
}),
]);

const V4PipelineSchema = z.union([
z.string(),
z.object({
Expand Down Expand Up @@ -867,7 +875,15 @@ export type V4WorkerOptionsShape = {
})
>
| string[];
hyperdrives?: Record<string, string | URL>;
hyperdrives?: Record<
string,
| string
| URL
| {
id?: string;
remoteProxyConnectionString: RemoteProxyConnectionString;
}
>;
ratelimits?: Record<
string,
{ namespace_id: string; simple: { limit: number; period?: 10 | 60 } }
Expand Down
21 changes: 15 additions & 6 deletions packages/miniflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -879,7 +879,7 @@ export class Miniflare {
readonly #initPromise: Promise<void>;

// Aborted when dispose() is called
readonly #disposeController: AbortController;
readonly #disposeController = new AbortController();
#loopbackServer?: StoppableServer;
#loopbackHost?: string;
readonly #webSocketServer: WebSocketServer;
Expand All @@ -891,7 +891,7 @@ export class Miniflare {

#containerPrivilegesCache = new ContainerPrivilegesCache();
#hyperdriveProxyController: HyperdriveProxyController =
new HyperdriveProxyController();
new HyperdriveProxyController(this.#disposeController.signal);

// Set the first time the remote-bindings proxy CLIENT worker reports a
// Cloudflare Access 403 block on a request to the remote-bindings proxy
Expand Down Expand Up @@ -1028,7 +1028,6 @@ export class Miniflare {
this.#devRegistry.dispose();
});

this.#disposeController = new AbortController();
this.#runtimeMutex = new Mutex();
this.#initPromise = this.#runtimeMutex
.runWith(async () => {
Expand Down Expand Up @@ -2675,6 +2674,14 @@ export class Miniflare {
this.#hyperdriveProxyController.beginUpdate();
try {
await this.#assembleAndUpdateConfigInternal(reusePorts);
} catch (error) {
if (
!this.#disposeController.signal.aborted ||
!(error instanceof Error) ||
error.name !== "AbortError"
) {
throw error;
}
} finally {
// If assembly or runtime startup failed, keep the old listeners and
// close only the new candidates. A successful update already committed.
Expand Down Expand Up @@ -3433,7 +3440,10 @@ export class Miniflare {

// Populate bindings from each plugin
for (const [key, plugin] of this.#mergedPluginEntries) {
const pluginBindings = await plugin.getNodeBindings(workerOpts);
const pluginBindings = await plugin.getNodeBindings(workerOpts, {
hyperdriveProxyController: this.#hyperdriveProxyController,
workerIndex,
});
for (const [name, binding] of Object.entries(pluginBindings)) {
if (binding instanceof ProxyNodeBinding) {
const proxyBindingName = getProxyBindingName(key, workerName, name);
Expand Down Expand Up @@ -3955,8 +3965,7 @@ export class Miniflare {
// Unregister workers from dev registry and stop the file watcher
await this.#devRegistry.dispose();

// shutdown hyperdrive proxies if any exist
await this.#hyperdriveProxyController.dispose();
this.#hyperdriveProxyController.dispose();

// Remove from instance registry as last step in disposal, preserving the
// existing behavior when an earlier cleanup operation fails.
Expand Down
Loading
Loading