diff --git a/packages/miniflare/src/plugins/agent-memory/index.ts b/packages/miniflare/src/plugins/agent-memory/index.ts index b9630e26a4..5a04bab021 100644 --- a/packages/miniflare/src/plugins/agent-memory/index.ts +++ b/packages/miniflare/src/plugins/agent-memory/index.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { - getUserBindingServiceName, + buildRemoteProxyProps, ProxyNodeBinding, remoteProxyClientWorker, } from "../shared"; @@ -20,6 +20,7 @@ export const AgentMemoryOptionsSchema = z.object({ export const AGENT_MEMORY_PLUGIN_NAME = "agent-memory"; const AGENT_MEMORY_SCOPE = "agent-memory"; +const AGENT_MEMORY_REMOTE_SERVICE_NAME = `${AGENT_MEMORY_SCOPE}:remote`; export const AGENT_MEMORY_PLUGIN: Plugin = { options: AgentMemoryOptionsSchema, @@ -32,10 +33,10 @@ export const AGENT_MEMORY_PLUGIN: Plugin = { return Object.entries(options.agentMemory).map(([bindingName, entry]) => ({ name: bindingName, service: { - name: getUserBindingServiceName( - AGENT_MEMORY_SCOPE, - bindingName, - entry.remoteProxyConnectionString + name: AGENT_MEMORY_REMOTE_SERVICE_NAME, + props: buildRemoteProxyProps( + entry.remoteProxyConnectionString, + bindingName ), }, })); @@ -53,20 +54,15 @@ export const AGENT_MEMORY_PLUGIN: Plugin = { ); }, async getServices({ options }) { - if (!options.agentMemory) { + if (!options.agentMemory || Object.keys(options.agentMemory).length === 0) { return []; } - return Object.entries(options.agentMemory).map(([bindingName, entry]) => ({ - name: getUserBindingServiceName( - AGENT_MEMORY_SCOPE, - bindingName, - entry.remoteProxyConnectionString - ), - worker: remoteProxyClientWorker( - entry.remoteProxyConnectionString, - bindingName - ), - })); + return [ + { + name: AGENT_MEMORY_REMOTE_SERVICE_NAME, + worker: remoteProxyClientWorker(), + }, + ]; }, }; diff --git a/packages/miniflare/src/plugins/ai-search/index.ts b/packages/miniflare/src/plugins/ai-search/index.ts index 27ed2d69be..0c40ab1352 100644 --- a/packages/miniflare/src/plugins/ai-search/index.ts +++ b/packages/miniflare/src/plugins/ai-search/index.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { - getUserBindingServiceName, + buildRemoteProxyProps, ProxyNodeBinding, remoteProxyClientWorker, } from "../shared"; @@ -21,10 +21,8 @@ export const AISearchOptionsSchema = z.object({ export const AI_SEARCH_PLUGIN_NAME = "ai-search"; -// Distinct scopes for service name generation to avoid collisions -// between namespace and instance bindings with the same binding name. -const AI_SEARCH_NS_SCOPE = "ai-search-ns"; -const AI_SEARCH_INST_SCOPE = "ai-search-inst"; +// One shared remote-proxy service for all AI Search bindings (config via props). +const AI_SEARCH_REMOTE_SERVICE_NAME = `${AI_SEARCH_PLUGIN_NAME}:remote`; export const AI_SEARCH_PLUGIN: Plugin = { options: AISearchOptionsSchema, @@ -32,34 +30,20 @@ export const AI_SEARCH_PLUGIN: Plugin = { async getBindings(options) { const bindings: { name: string; - service: { name: string }; + service: { name: string; props?: { json: string } }; }[] = []; - for (const [bindingName, entry] of Object.entries( - options.aiSearchNamespaces ?? {} - )) { + for (const [bindingName, entry] of [ + ...Object.entries(options.aiSearchNamespaces ?? {}), + ...Object.entries(options.aiSearchInstances ?? {}), + ]) { bindings.push({ name: bindingName, service: { - name: getUserBindingServiceName( - AI_SEARCH_NS_SCOPE, - bindingName, - entry.remoteProxyConnectionString - ), - }, - }); - } - - for (const [bindingName, entry] of Object.entries( - options.aiSearchInstances ?? {} - )) { - bindings.push({ - name: bindingName, - service: { - name: getUserBindingServiceName( - AI_SEARCH_INST_SCOPE, - bindingName, - entry.remoteProxyConnectionString + name: AI_SEARCH_REMOTE_SERVICE_NAME, + props: buildRemoteProxyProps( + entry.remoteProxyConnectionString, + bindingName ), }, }); @@ -85,35 +69,13 @@ export const AI_SEARCH_PLUGIN: Plugin = { worker: ReturnType; }[] = []; - for (const [bindingName, entry] of Object.entries( - options.aiSearchNamespaces ?? {} - )) { - services.push({ - name: getUserBindingServiceName( - AI_SEARCH_NS_SCOPE, - bindingName, - entry.remoteProxyConnectionString - ), - worker: remoteProxyClientWorker( - entry.remoteProxyConnectionString, - bindingName - ), - }); - } - - for (const [bindingName, entry] of Object.entries( - options.aiSearchInstances ?? {} - )) { + const hasAny = + Object.keys(options.aiSearchNamespaces ?? {}).length > 0 || + Object.keys(options.aiSearchInstances ?? {}).length > 0; + if (hasAny) { services.push({ - name: getUserBindingServiceName( - AI_SEARCH_INST_SCOPE, - bindingName, - entry.remoteProxyConnectionString - ), - worker: remoteProxyClientWorker( - entry.remoteProxyConnectionString, - bindingName - ), + name: AI_SEARCH_REMOTE_SERVICE_NAME, + worker: remoteProxyClientWorker(), }); } diff --git a/packages/miniflare/src/plugins/ai/index.ts b/packages/miniflare/src/plugins/ai/index.ts index 649d943583..b24616f49f 100644 --- a/packages/miniflare/src/plugins/ai/index.ts +++ b/packages/miniflare/src/plugins/ai/index.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { - getUserBindingServiceName, + buildRemoteProxyProps, ProxyNodeBinding, remoteProxyClientWorker, } from "../shared"; @@ -18,6 +18,7 @@ export const AIOptionsSchema = z.object({ }); export const AI_PLUGIN_NAME = "ai"; +const AI_REMOTE_SERVICE_NAME = `${AI_PLUGIN_NAME}:remote`; export const AI_PLUGIN: Plugin = { options: AIOptionsSchema, @@ -36,10 +37,10 @@ export const AI_PLUGIN: Plugin = { { name: "fetcher", service: { - name: getUserBindingServiceName( - AI_PLUGIN_NAME, - options.ai.binding, - options.ai.remoteProxyConnectionString + name: AI_REMOTE_SERVICE_NAME, + props: buildRemoteProxyProps( + options.ai.remoteProxyConnectionString, + options.ai.binding ), }, }, @@ -63,15 +64,8 @@ export const AI_PLUGIN: Plugin = { return [ { - name: getUserBindingServiceName( - AI_PLUGIN_NAME, - options.ai.binding, - options.ai.remoteProxyConnectionString - ), - worker: remoteProxyClientWorker( - options.ai.remoteProxyConnectionString, - options.ai.binding - ), + name: AI_REMOTE_SERVICE_NAME, + worker: remoteProxyClientWorker(), }, ]; }, diff --git a/packages/miniflare/src/plugins/artifacts/index.ts b/packages/miniflare/src/plugins/artifacts/index.ts index 881ddfd217..79231159fa 100644 --- a/packages/miniflare/src/plugins/artifacts/index.ts +++ b/packages/miniflare/src/plugins/artifacts/index.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { - getUserBindingServiceName, + buildRemoteProxyProps, ProxyNodeBinding, remoteProxyClientWorker, } from "../shared"; @@ -18,6 +18,9 @@ export const ArtifactsOptionsSchema = z.object({ }); export const ARTIFACTS_PLUGIN_NAME = "artifacts"; +// One shared remote-proxy service for every artifacts binding; per-binding +// config travels via props. +const ARTIFACTS_REMOTE_SERVICE_NAME = `${ARTIFACTS_PLUGIN_NAME}:remote`; export const ARTIFACTS_PLUGIN: Plugin = { options: ArtifactsOptionsSchema, @@ -30,11 +33,8 @@ export const ARTIFACTS_PLUGIN: Plugin = { return Object.entries(options.artifacts).map(([name, config]) => ({ name, service: { - name: getUserBindingServiceName( - ARTIFACTS_PLUGIN_NAME, - name, - config.remoteProxyConnectionString - ), + name: ARTIFACTS_REMOTE_SERVICE_NAME, + props: buildRemoteProxyProps(config.remoteProxyConnectionString, name), }, })); }, @@ -50,19 +50,15 @@ export const ARTIFACTS_PLUGIN: Plugin = { ); }, async getServices({ options }) { - if (!options.artifacts) { + if (!options.artifacts || Object.keys(options.artifacts).length === 0) { return []; } - return Object.entries(options.artifacts).map( - ([name, { remoteProxyConnectionString }]) => ({ - name: getUserBindingServiceName( - ARTIFACTS_PLUGIN_NAME, - name, - remoteProxyConnectionString - ), - worker: remoteProxyClientWorker(remoteProxyConnectionString, name), - }) - ); + return [ + { + name: ARTIFACTS_REMOTE_SERVICE_NAME, + worker: remoteProxyClientWorker(), + }, + ]; }, }; diff --git a/packages/miniflare/src/plugins/browser-rendering/index.ts b/packages/miniflare/src/plugins/browser-rendering/index.ts index 3bd0fff475..8ac011b0b2 100644 --- a/packages/miniflare/src/plugins/browser-rendering/index.ts +++ b/packages/miniflare/src/plugins/browser-rendering/index.ts @@ -18,6 +18,7 @@ import BROWSER_RENDERING_WORKER from "worker:browser-rendering/binding"; import { z } from "zod"; import { kVoid } from "../../runtime"; import { + buildRemoteProxyProps, getUserBindingServiceName, ProxyNodeBinding, remoteProxyClientWorker, @@ -40,6 +41,7 @@ export const BrowserRenderingOptionsSchema = z.object({ }); export const BROWSER_RENDERING_PLUGIN_NAME = "browser-rendering"; +const BROWSER_RENDERING_REMOTE_SERVICE_NAME = `${BROWSER_RENDERING_PLUGIN_NAME}:remote`; export const BROWSER_RENDERING_PLUGIN: Plugin< typeof BrowserRenderingOptionsSchema @@ -54,13 +56,20 @@ export const BROWSER_RENDERING_PLUGIN: Plugin< return [ { name: options.browserRendering.binding, - service: { - name: getUserBindingServiceName( - BROWSER_RENDERING_PLUGIN_NAME, - "service", - options.browserRendering.remoteProxyConnectionString - ), - }, + service: options.browserRendering.remoteProxyConnectionString + ? { + name: BROWSER_RENDERING_REMOTE_SERVICE_NAME, + props: buildRemoteProxyProps( + options.browserRendering.remoteProxyConnectionString, + options.browserRendering.binding + ), + } + : { + name: getUserBindingServiceName( + BROWSER_RENDERING_PLUGIN_NAME, + "service" + ), + }, }, ]; }, @@ -77,44 +86,47 @@ export const BROWSER_RENDERING_PLUGIN: Plugin< return []; } + if (options.browserRendering.remoteProxyConnectionString) { + return [ + { + name: BROWSER_RENDERING_REMOTE_SERVICE_NAME, + worker: remoteProxyClientWorker(), + }, + ]; + } + return [ { name: getUserBindingServiceName( BROWSER_RENDERING_PLUGIN_NAME, - "service", - options.browserRendering.remoteProxyConnectionString + "service" ), - worker: options.browserRendering.remoteProxyConnectionString - ? remoteProxyClientWorker( - options.browserRendering.remoteProxyConnectionString, - options.browserRendering.binding - ) - : { - compatibilityDate: "2025-05-01", - compatibilityFlags: ["nodejs_compat"], - modules: [ - { - name: "index.worker.js", - esModule: BROWSER_RENDERING_WORKER(), - }, - ], - bindings: [ - WORKER_BINDING_SERVICE_LOOPBACK, - { - name: "BrowserSession", - durableObjectNamespace: { - className: "BrowserSession", - }, - }, - ], - durableObjectNamespaces: [ - { - className: "BrowserSession", - uniqueKey: "miniflare-BrowserSession", - }, - ], - durableObjectStorage: { inMemory: kVoid }, + worker: { + compatibilityDate: "2025-05-01", + compatibilityFlags: ["nodejs_compat"], + modules: [ + { + name: "index.worker.js", + esModule: BROWSER_RENDERING_WORKER(), + }, + ], + bindings: [ + WORKER_BINDING_SERVICE_LOOPBACK, + { + name: "BrowserSession", + durableObjectNamespace: { + className: "BrowserSession", + }, }, + ], + durableObjectNamespaces: [ + { + className: "BrowserSession", + uniqueKey: "miniflare-BrowserSession", + }, + ], + durableObjectStorage: { inMemory: kVoid }, + }, }, ]; }, diff --git a/packages/miniflare/src/plugins/core/explorer.ts b/packages/miniflare/src/plugins/core/explorer.ts index 022e3ca69f..6025927588 100644 --- a/packages/miniflare/src/plugins/core/explorer.ts +++ b/packages/miniflare/src/plugins/core/explorer.ts @@ -7,7 +7,7 @@ import { type Worker_Binding, type Worker_Module, } from "../../runtime"; -import { CoreBindings } from "../../workers"; +import { CoreBindings, SharedBindings } from "../../workers"; import { normaliseDurableObject } from "../do"; import { namespaceEntries, @@ -177,10 +177,18 @@ export function constructExplorerBindingMap( const databaseId = innerBinding.service?.name?.replace(/^d1:db:/, ""); assert(databaseId); - IDToBindingName.d1[databaseId] = binding.name; + // Remote databases share one proxy service ("d1:db:remote"). Remote + // resources aren't surfaced in the explorer, so skip them — otherwise + // they'd all collide under the literal id "remote". + if (databaseId !== "remote") { + IDToBindingName.d1[databaseId] = binding.name; + } } - // KV bindings: name = "MINIFLARE_PROXY:kv:worker:BINDING", kvNamespace.name = "kv:ns:ID" + // KV bindings: name = "MINIFLARE_PROXY:kv:worker:BINDING". + // Local namespaces share one entry service ("kv:ns:entry") and carry their + // id in props; remote namespaces share one proxy service ("kv:ns:remote") + // and aren't surfaced in the explorer. if ( binding.name?.startsWith( `${CoreBindings.DURABLE_OBJECT_NAMESPACE_PROXY}:kv:` @@ -188,9 +196,24 @@ export function constructExplorerBindingMap( "kvNamespace" in binding && binding.kvNamespace?.name?.startsWith("kv:ns:") ) { - // Extract ID from service name "kv:ns:ID" - const namespaceId = binding.kvNamespace.name.replace(/^kv:ns:/, ""); - IDToBindingName.kv[namespaceId] = binding.name; + let namespaceId: string | undefined; + const propsJson = binding.kvNamespace.props?.json; + if (propsJson !== undefined) { + try { + namespaceId = JSON.parse(propsJson)[SharedBindings.TEXT_NAMESPACE]; + } catch { + // fall through to service-name parsing + } + } + if (namespaceId === undefined) { + namespaceId = binding.kvNamespace.name.replace(/^kv:ns:/, ""); + } + // Remote namespaces share one proxy service ("kv:ns:remote"). Remote + // resources aren't surfaced in the explorer, so skip them — otherwise + // they'd all collide under the literal id "remote". + if (namespaceId !== "remote") { + IDToBindingName.kv[namespaceId] = binding.name; + } } // R2 bindings: name = "MINIFLARE_PROXY:r2:worker:BINDING", r2Bucket.name = "r2:bucket:ID" @@ -203,7 +226,12 @@ export function constructExplorerBindingMap( ) { // Extract bucket name from service name "r2:bucket:BUCKET_NAME" const bucketName = binding.r2Bucket.name.replace(/^r2:bucket:/, ""); - IDToBindingName.r2[bucketName] = binding.name; + // Remote buckets share one proxy service ("r2:bucket:remote"). Remote + // resources aren't surfaced in the explorer, so skip them — otherwise + // they'd all collide under the literal id "remote". + if (bucketName !== "remote") { + IDToBindingName.r2[bucketName] = binding.name; + } } } diff --git a/packages/miniflare/src/plugins/core/index.ts b/packages/miniflare/src/plugins/core/index.ts index 49c8a7fa6c..0c8d608b7e 100644 --- a/packages/miniflare/src/plugins/core/index.ts +++ b/packages/miniflare/src/plugins/core/index.ts @@ -35,6 +35,7 @@ import { import { IMAGES_PLUGIN_NAME } from "../images"; import { getR2PublicService, R2_PUBLIC_SERVICE_NAME } from "../r2"; import { + buildRemoteProxyProps, getUserBindingServiceName, parseRoutes, ProxyNodeBinding, @@ -436,6 +437,8 @@ function getCustomServiceDesignator( } else if ("remoteProxyConnectionString" in service) { assert("name" in service && typeof service.name === "string"); serviceName = `${CORE_PLUGIN_NAME}:remote-proxy-service:${workerIndex}:${name}`; + // Per-binding remote config travels via props to a generic proxy worker. + props = buildRemoteProxyProps(service.remoteProxyConnectionString, name); } // Worker with entrypoint else if ("name" in service) { @@ -524,10 +527,7 @@ function maybeGetCustomServiceService( return { name: `${CORE_PLUGIN_NAME}:remote-proxy-service:${workerIndex}:${name}`, - worker: remoteProxyClientWorker( - service.remoteProxyConnectionString, - name - ), + worker: remoteProxyClientWorker(), }; } } diff --git a/packages/miniflare/src/plugins/d1/index.ts b/packages/miniflare/src/plugins/d1/index.ts index 3dee2a65c0..007f73d68e 100644 --- a/packages/miniflare/src/plugins/d1/index.ts +++ b/packages/miniflare/src/plugins/d1/index.ts @@ -4,6 +4,7 @@ import SCRIPT_D1_DATABASE_OBJECT from "worker:d1/database"; import { z } from "zod"; import { SharedBindings } from "../../workers"; import { + buildRemoteProxyProps, getMiniflareObjectBindings, getPersistPath, getUserBindingServiceName, @@ -48,6 +49,8 @@ export const D1SharedOptionsSchema = z.object({ export const D1_PLUGIN_NAME = "d1"; const D1_STORAGE_SERVICE_NAME = `${D1_PLUGIN_NAME}:storage`; const D1_DATABASE_SERVICE_PREFIX = `${D1_PLUGIN_NAME}:db`; +// One shared remote-proxy service for all remote D1 databases (config via props). +const D1_REMOTE_SERVICE_NAME = `${D1_PLUGIN_NAME}:db:remote`; const D1_DATABASE_OBJECT_CLASS_NAME = "D1DatabaseObject"; const D1_DATABASE_OBJECT: Worker_Binding_DurableObjectNamespaceDesignator = { serviceName: D1_DATABASE_SERVICE_PREFIX, @@ -70,18 +73,21 @@ export const D1_PLUGIN: Plugin< "Alpha D1 Databases cannot run remotely" ); - const serviceName = getUserBindingServiceName( - D1_DATABASE_SERVICE_PREFIX, - id, - remoteProxyConnectionString - ); + // Remote databases share one proxy service (config via props); + // local databases keep their per-id entry service. + const serviceDesignator = remoteProxyConnectionString + ? { + name: D1_REMOTE_SERVICE_NAME, + props: buildRemoteProxyProps(remoteProxyConnectionString, name), + } + : { + name: getUserBindingServiceName(D1_DATABASE_SERVICE_PREFIX, id), + }; const binding = name.startsWith("__D1_BETA__") ? // Used before Wrangler 3.3 { - service: { - name: serviceName, - }, + service: serviceDesignator, } : // Used after Wrangler 3.3 { @@ -90,9 +96,7 @@ export const D1_PLUGIN: Plugin< innerBindings: [ { name: "fetcher", - service: { - name: serviceName, - }, + service: serviceDesignator, }, ], }, @@ -118,20 +122,28 @@ export const D1_PLUGIN: Plugin< }) { const persist = sharedOptions.d1Persist; const databases = namespaceEntries(options.d1Databases); - const services = databases.map( - ([name, { id, remoteProxyConnectionString }]) => ({ - name: getUserBindingServiceName( - D1_DATABASE_SERVICE_PREFIX, - id, - remoteProxyConnectionString - ), - worker: remoteProxyConnectionString - ? remoteProxyClientWorker(remoteProxyConnectionString, name) - : objectEntryWorker(D1_DATABASE_OBJECT, id), - }) - ); - if (databases.length > 0) { + const services: Service[] = []; + let hasRemote = false; + for (const [, { id, remoteProxyConnectionString }] of databases) { + if (remoteProxyConnectionString) { + hasRemote = true; + } else { + services.push({ + name: getUserBindingServiceName(D1_DATABASE_SERVICE_PREFIX, id), + worker: objectEntryWorker(D1_DATABASE_OBJECT, id), + }); + } + } + if (hasRemote) { + services.push({ + name: D1_REMOTE_SERVICE_NAME, + worker: remoteProxyClientWorker(), + }); + } + + const hasLocal = services.some((s) => s.name !== D1_REMOTE_SERVICE_NAME); + if (hasLocal) { const uniqueKey = `miniflare-${D1_DATABASE_OBJECT_CLASS_NAME}`; const persistPath = getPersistPath( D1_PLUGIN_NAME, @@ -180,8 +192,11 @@ export const D1_PLUGIN: Plugin< }; services.push(storageService, objectService); - for (const database of databases) { - await migrateDatabase(log, uniqueKey, persistPath, database[1].id); + for (const [, database] of databases) { + if (database.remoteProxyConnectionString) { + continue; + } + await migrateDatabase(log, uniqueKey, persistPath, database.id); } } diff --git a/packages/miniflare/src/plugins/dispatch-namespace/index.ts b/packages/miniflare/src/plugins/dispatch-namespace/index.ts index 3f387ab7b3..11e4c77631 100644 --- a/packages/miniflare/src/plugins/dispatch-namespace/index.ts +++ b/packages/miniflare/src/plugins/dispatch-namespace/index.ts @@ -2,7 +2,7 @@ import SCRIPT_DISPATCH_NAMESPACE from "worker:dispatch-namespace/dispatch-namesp import SCRIPT_DISPATCH_NAMESPACE_PROXY from "worker:dispatch-namespace/dispatch-namespace-proxy"; import { z } from "zod"; import { - getUserBindingServiceName, + buildRemoteProxyProps, ProxyNodeBinding, remoteProxyClientWorker, } from "../shared"; @@ -24,17 +24,8 @@ export const DispatchNamespaceOptionsSchema = z.object({ export const DISPATCH_NAMESPACE_PLUGIN_NAME = "dispatch-namespace"; -/** Service name for the proxy client worker backing a dispatch namespace. */ -function getProxyServiceName( - name: string, - remoteProxyConnectionString?: RemoteProxyConnectionString -): string { - return getUserBindingServiceName( - `${DISPATCH_NAMESPACE_PLUGIN_NAME}-proxy`, - name, - remoteProxyConnectionString - ); -} +// One shared proxy client service for all dispatch namespaces (config via props). +const DISPATCH_NAMESPACE_REMOTE_SERVICE_NAME = `${DISPATCH_NAMESPACE_PLUGIN_NAME}-proxy:remote`; export const DISPATCH_NAMESPACE_PLUGIN: Plugin< typeof DispatchNamespaceOptionsSchema @@ -57,9 +48,10 @@ export const DISPATCH_NAMESPACE_PLUGIN: Plugin< { name: "proxyClient", service: { - name: getProxyServiceName( - name, - config.remoteProxyConnectionString + name: DISPATCH_NAMESPACE_REMOTE_SERVICE_NAME, + props: buildRemoteProxyProps( + config.remoteProxyConnectionString, + name ), }, }, @@ -81,18 +73,19 @@ export const DISPATCH_NAMESPACE_PLUGIN: Plugin< ); }, async getServices({ options }) { - if (!options.dispatchNamespaces) { + if ( + !options.dispatchNamespaces || + Object.keys(options.dispatchNamespaces).length === 0 + ) { return []; } - return Object.entries(options.dispatchNamespaces).map(([name, config]) => ({ - name: getProxyServiceName(name, config.remoteProxyConnectionString), - worker: remoteProxyClientWorker( - config.remoteProxyConnectionString, - name, - SCRIPT_DISPATCH_NAMESPACE_PROXY - ), - })); + return [ + { + name: DISPATCH_NAMESPACE_REMOTE_SERVICE_NAME, + worker: remoteProxyClientWorker(SCRIPT_DISPATCH_NAMESPACE_PROXY), + }, + ]; }, getExtensions({ options }) { if (!options.some((o) => o.dispatchNamespaces)) { diff --git a/packages/miniflare/src/plugins/email/index.ts b/packages/miniflare/src/plugins/email/index.ts index 908d19c44e..d46d5c8024 100644 --- a/packages/miniflare/src/plugins/email/index.ts +++ b/packages/miniflare/src/plugins/email/index.ts @@ -4,6 +4,7 @@ import EMAIL_MESSAGE from "worker:email/email"; import SEND_EMAIL_BINDING from "worker:email/send_email"; import { z } from "zod"; import { + buildRemoteProxyProps, getUserBindingServiceName, remoteProxyClientWorker, ProxyNodeBinding, @@ -43,6 +44,7 @@ export const EmailOptionsSchema = z.object({ export const EMAIL_PLUGIN_NAME = "email"; const SERVICE_SEND_EMAIL_WORKER_PREFIX = `SEND-EMAIL-WORKER`; +const EMAIL_REMOTE_SERVICE_NAME = `${EMAIL_PLUGIN_NAME}:remote`; // Disk service name and binding name for writing temporary files to system temp directory const EMAIL_DISK_SERVICE_NAME = `${EMAIL_PLUGIN_NAME}:disk`; const EMAIL_DISK_BINDING_NAME = "MINIFLARE_EMAIL_DISK"; @@ -112,12 +114,18 @@ export const EMAIL_PLUGIN: Plugin = { return sendEmailBindings.map(({ name, remoteProxyConnectionString }) => ({ name, - service: { - entrypoint: remoteProxyConnectionString - ? undefined - : "SendEmailBinding", - name: getUserBindingServiceName(SERVICE_SEND_EMAIL_WORKER_PREFIX, name), - }, + service: remoteProxyConnectionString + ? { + name: EMAIL_REMOTE_SERVICE_NAME, + props: buildRemoteProxyProps(remoteProxyConnectionString, name), + } + : { + entrypoint: "SendEmailBinding", + name: getUserBindingServiceName( + SERVICE_SEND_EMAIL_WORKER_PREFIX, + name + ), + }, })); }, getNodeBindings(options) { @@ -179,32 +187,42 @@ export const EMAIL_PLUGIN: Plugin = { }, })); + let hasRemote = false; for (const { name, remoteProxyConnectionString, ...config } of args.options .email?.send_email ?? []) { + if (remoteProxyConnectionString) { + hasRemote = true; + continue; + } services.push({ name: getUserBindingServiceName(SERVICE_SEND_EMAIL_WORKER_PREFIX, name), - worker: remoteProxyConnectionString - ? remoteProxyClientWorker(remoteProxyConnectionString, name) - : { - compatibilityDate: "2025-03-17", - modules: [ - { - name: "send_email.mjs", - esModule: SEND_EMAIL_BINDING(), - }, - ], - bindings: [ - ...buildJsonBindings(config), - ...diskServices.map(({ bindingName, serviceName }) => ({ - name: bindingName, - service: { name: serviceName }, - })), - { - name: "email_disk_services", - json: JSON.stringify(diskServices), - }, - ], + worker: { + compatibilityDate: "2025-03-17", + modules: [ + { + name: "send_email.mjs", + esModule: SEND_EMAIL_BINDING(), + }, + ], + bindings: [ + ...buildJsonBindings(config), + ...diskServices.map(({ bindingName, serviceName }) => ({ + name: bindingName, + service: { name: serviceName }, + })), + { + name: "email_disk_services", + json: JSON.stringify(diskServices), }, + ], + }, + }); + } + + if (hasRemote) { + services.push({ + name: EMAIL_REMOTE_SERVICE_NAME, + worker: remoteProxyClientWorker(), }); } diff --git a/packages/miniflare/src/plugins/flagship/index.ts b/packages/miniflare/src/plugins/flagship/index.ts index 136eb45f31..8c30d56aa3 100644 --- a/packages/miniflare/src/plugins/flagship/index.ts +++ b/packages/miniflare/src/plugins/flagship/index.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { - getUserBindingServiceName, + buildRemoteProxyProps, ProxyNodeBinding, remoteProxyClientWorker, } from "../shared"; @@ -19,6 +19,7 @@ export const FlagshipOptionsSchema = z.object({ }); export const FLAGSHIP_PLUGIN_NAME = "flagship"; +const FLAGSHIP_REMOTE_SERVICE_NAME = `${FLAGSHIP_PLUGIN_NAME}:remote`; export const FLAGSHIP_PLUGIN: Plugin = { options: FlagshipOptionsSchema, @@ -32,10 +33,10 @@ export const FLAGSHIP_PLUGIN: Plugin = { ([name, config]) => ({ name, service: { - name: getUserBindingServiceName( - FLAGSHIP_PLUGIN_NAME, - name, - config.remoteProxyConnectionString + name: FLAGSHIP_REMOTE_SERVICE_NAME, + props: buildRemoteProxyProps( + config.remoteProxyConnectionString, + name ), }, }) @@ -53,21 +54,15 @@ export const FLAGSHIP_PLUGIN: Plugin = { ); }, async getServices({ options }) { - if (!options.flagship) { + if (!options.flagship || Object.keys(options.flagship).length === 0) { return []; } - return Object.entries(options.flagship).map( - ([name, { remoteProxyConnectionString }]) => { - return { - name: getUserBindingServiceName( - FLAGSHIP_PLUGIN_NAME, - name, - remoteProxyConnectionString - ), - worker: remoteProxyClientWorker(remoteProxyConnectionString, name), - }; - } - ); + return [ + { + name: FLAGSHIP_REMOTE_SERVICE_NAME, + worker: remoteProxyClientWorker(), + }, + ]; }, }; diff --git a/packages/miniflare/src/plugins/images/index.ts b/packages/miniflare/src/plugins/images/index.ts index c729aa38e7..7df446df28 100644 --- a/packages/miniflare/src/plugins/images/index.ts +++ b/packages/miniflare/src/plugins/images/index.ts @@ -5,6 +5,7 @@ import { z } from "zod"; import { SharedBindings } from "../../workers"; import { KV_NAMESPACE_OBJECT_CLASS_NAME } from "../kv"; import { + buildRemoteProxyProps, getMiniflareObjectBindings, getPersistPath, getUserBindingServiceName, @@ -34,6 +35,7 @@ export const ImagesSharedOptionsSchema = z.object({ }); export const IMAGES_PLUGIN_NAME = "images"; +const IMAGES_REMOTE_SERVICE_NAME = `${IMAGES_PLUGIN_NAME}:remote`; export const IMAGES_PLUGIN: Plugin< typeof ImagesOptionsSchema, @@ -55,13 +57,20 @@ export const IMAGES_PLUGIN: Plugin< innerBindings: [ { name: "fetcher", - service: { - name: getUserBindingServiceName( - IMAGES_PLUGIN_NAME, - options.images.binding, - options.images.remoteProxyConnectionString - ), - }, + service: options.images.remoteProxyConnectionString + ? { + name: IMAGES_REMOTE_SERVICE_NAME, + props: buildRemoteProxyProps( + options.images.remoteProxyConnectionString, + options.images.binding + ), + } + : { + name: getUserBindingServiceName( + IMAGES_PLUGIN_NAME, + options.images.binding + ), + }, }, ], }, @@ -87,24 +96,20 @@ export const IMAGES_PLUGIN: Plugin< return []; } - const serviceName = getUserBindingServiceName( - IMAGES_PLUGIN_NAME, - options.images.binding, - options.images.remoteProxyConnectionString - ); - if (options.images.remoteProxyConnectionString) { return [ { - name: serviceName, - worker: remoteProxyClientWorker( - options.images.remoteProxyConnectionString, - options.images.binding - ), + name: IMAGES_REMOTE_SERVICE_NAME, + worker: remoteProxyClientWorker(), }, ]; } + const serviceName = getUserBindingServiceName( + IMAGES_PLUGIN_NAME, + options.images.binding + ); + const persistPath = getPersistPath( IMAGES_PLUGIN_NAME, tmpPath, diff --git a/packages/miniflare/src/plugins/kv/index.ts b/packages/miniflare/src/plugins/kv/index.ts index ffcbd0d74b..ff79ca4100 100644 --- a/packages/miniflare/src/plugins/kv/index.ts +++ b/packages/miniflare/src/plugins/kv/index.ts @@ -4,9 +4,9 @@ import { z } from "zod"; import { PathSchema } from "../../shared"; import { SharedBindings } from "../../workers"; import { + buildRemoteProxyProps, getMiniflareObjectBindings, getPersistPath, - getUserBindingServiceName, migrateDatabase, namespaceEntries, namespaceKeys, @@ -58,6 +58,11 @@ export const KVSharedOptionsSchema = z.object({ }); const SERVICE_NAMESPACE_PREFIX = `${KV_PLUGIN_NAME}:ns`; +// A single entry service shared by every *local* namespace. Each namespace's id +// is supplied per-binding via `ctx.props`, so one service serves all of them. +const KV_LOCAL_ENTRY_SERVICE_NAME = `${KV_PLUGIN_NAME}:ns:entry`; +// One shared remote-proxy service for all remote namespaces (config via props). +const KV_REMOTE_SERVICE_NAME = `${KV_PLUGIN_NAME}:ns:remote`; const KV_STORAGE_SERVICE_NAME = `${KV_PLUGIN_NAME}:storage`; export const KV_NAMESPACE_OBJECT_CLASS_NAME = "KVNamespaceObject"; const KV_NAMESPACE_OBJECT: Worker_Binding_DurableObjectNamespaceDesignator = { @@ -80,16 +85,35 @@ export const KV_PLUGIN: Plugin< bindingTypeDescription: "KV namespace", async getBindings(options) { const namespaces = namespaceEntries(options.kvNamespaces); - const bindings = namespaces.map(([name, namespace]) => ({ - name, - kvNamespace: { - name: getUserBindingServiceName( - SERVICE_NAMESPACE_PREFIX, - namespace.id, - namespace.remoteProxyConnectionString - ), - }, - })); + const bindings = namespaces.map(([name, namespace]) => { + // Remote (mixed-mode) namespaces share one proxy service; per-binding + // config (connection string) travels via props. + if (namespace.remoteProxyConnectionString) { + return { + name, + kvNamespace: { + name: KV_REMOTE_SERVICE_NAME, + props: buildRemoteProxyProps( + namespace.remoteProxyConnectionString, + name + ), + }, + }; + } + // Local namespaces all share one entry service; the namespace id is + // passed at runtime via props (read in object-entry.worker.ts). + return { + name, + kvNamespace: { + name: KV_LOCAL_ENTRY_SERVICE_NAME, + props: { + json: JSON.stringify({ + [SharedBindings.TEXT_NAMESPACE]: namespace.id, + }), + }, + }, + }; + }); if (isWorkersSitesEnabled(options)) { bindings.push(...(await getSitesBindings(options))); @@ -121,20 +145,32 @@ export const KV_PLUGIN: Plugin< }) { const persist = sharedOptions.kvPersist; const namespaces = namespaceEntries(options.kvNamespaces); - const services = namespaces.map( - ([name, { id, remoteProxyConnectionString }]) => ({ - name: getUserBindingServiceName( - SERVICE_NAMESPACE_PREFIX, - id, - remoteProxyConnectionString - ), - worker: remoteProxyConnectionString - ? remoteProxyClientWorker(remoteProxyConnectionString, name) - : objectEntryWorker(KV_NAMESPACE_OBJECT, id), - }) + + const services: Service[] = []; + + // One shared entry service for all local namespaces (id supplied via props). + const hasLocalNamespace = namespaces.some( + ([, ns]) => !ns.remoteProxyConnectionString ); + if (hasLocalNamespace) { + services.push({ + name: KV_LOCAL_ENTRY_SERVICE_NAME, + worker: objectEntryWorker(KV_NAMESPACE_OBJECT), + }); + } + + // One shared proxy service for all remote (mixed-mode) namespaces. + const hasRemoteNamespace = namespaces.some( + ([, ns]) => ns.remoteProxyConnectionString + ); + if (hasRemoteNamespace) { + services.push({ + name: KV_REMOTE_SERVICE_NAME, + worker: remoteProxyClientWorker(), + }); + } - if (services.length > 0) { + if (hasLocalNamespace) { const uniqueKey = `miniflare-${KV_NAMESPACE_OBJECT_CLASS_NAME}`; const persistPath = getPersistPath( KV_PLUGIN_NAME, @@ -184,8 +220,11 @@ export const KV_PLUGIN: Plugin< // another breaking change to the persistence location, migrate SQLite // databases from the old location to the new location. Blobs are still // stored in the same location. - for (const namespace of namespaces) { - await migrateDatabase(log, uniqueKey, persistPath, namespace[1].id); + for (const [, namespace] of namespaces) { + if (namespace.remoteProxyConnectionString) { + continue; + } + await migrateDatabase(log, uniqueKey, persistPath, namespace.id); } } diff --git a/packages/miniflare/src/plugins/media/index.ts b/packages/miniflare/src/plugins/media/index.ts index 7c0583290a..5ac1fda46a 100644 --- a/packages/miniflare/src/plugins/media/index.ts +++ b/packages/miniflare/src/plugins/media/index.ts @@ -1,12 +1,13 @@ import { z } from "zod"; import { - getUserBindingServiceName, + buildRemoteProxyProps, ProxyNodeBinding, remoteProxyClientWorker, } from "../shared"; import type { Plugin, RemoteProxyConnectionString } from "../shared"; export const MEDIA_PLUGIN_NAME = "media"; +const MEDIA_REMOTE_SERVICE_NAME = `${MEDIA_PLUGIN_NAME}:remote`; const MediaSchema = z.object({ binding: z.string(), @@ -31,10 +32,10 @@ export const MEDIA_PLUGIN: Plugin = { { name: options.media.binding, service: { - name: getUserBindingServiceName( - MEDIA_PLUGIN_NAME, - options.media.binding, - options.media.remoteProxyConnectionString + name: MEDIA_REMOTE_SERVICE_NAME, + props: buildRemoteProxyProps( + options.media.remoteProxyConnectionString, + options.media.binding ), }, }, @@ -55,15 +56,8 @@ export const MEDIA_PLUGIN: Plugin = { return [ { - name: getUserBindingServiceName( - MEDIA_PLUGIN_NAME, - options.media.binding, - options.media.remoteProxyConnectionString - ), - worker: remoteProxyClientWorker( - options.media.remoteProxyConnectionString, - options.media.binding - ), + name: MEDIA_REMOTE_SERVICE_NAME, + worker: remoteProxyClientWorker(), }, ]; }, diff --git a/packages/miniflare/src/plugins/mtls/index.ts b/packages/miniflare/src/plugins/mtls/index.ts index dafd818094..5977b4dde7 100644 --- a/packages/miniflare/src/plugins/mtls/index.ts +++ b/packages/miniflare/src/plugins/mtls/index.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { - getUserBindingServiceName, + buildRemoteProxyProps, ProxyNodeBinding, remoteProxyClientWorker, } from "../shared"; @@ -18,6 +18,7 @@ export const MtlsOptionsSchema = z.object({ }); export const MTLS_PLUGIN_NAME = "mtls"; +const MTLS_REMOTE_SERVICE_NAME = `${MTLS_PLUGIN_NAME}:remote`; export const MTLS_PLUGIN: Plugin = { options: MtlsOptionsSchema, @@ -28,16 +29,13 @@ export const MTLS_PLUGIN: Plugin = { } return Object.entries(options.mtlsCertificates).map( - ([name, { certificate_id, remoteProxyConnectionString }]) => { + ([name, { remoteProxyConnectionString }]) => { return { name, service: { - name: getUserBindingServiceName( - MTLS_PLUGIN_NAME, - certificate_id, - remoteProxyConnectionString - ), + name: MTLS_REMOTE_SERVICE_NAME, + props: buildRemoteProxyProps(remoteProxyConnectionString, name), }, }; } @@ -55,21 +53,18 @@ export const MTLS_PLUGIN: Plugin = { ); }, async getServices({ options }) { - if (!options.mtlsCertificates) { + if ( + !options.mtlsCertificates || + Object.keys(options.mtlsCertificates).length === 0 + ) { return []; } - return Object.entries(options.mtlsCertificates).map( - ([name, { certificate_id, remoteProxyConnectionString }]) => { - return { - name: getUserBindingServiceName( - MTLS_PLUGIN_NAME, - certificate_id, - remoteProxyConnectionString - ), - worker: remoteProxyClientWorker(remoteProxyConnectionString, name), - }; - } - ); + return [ + { + name: MTLS_REMOTE_SERVICE_NAME, + worker: remoteProxyClientWorker(), + }, + ]; }, }; diff --git a/packages/miniflare/src/plugins/pipelines/index.ts b/packages/miniflare/src/plugins/pipelines/index.ts index a3c0d649c5..c65fc7f5e3 100644 --- a/packages/miniflare/src/plugins/pipelines/index.ts +++ b/packages/miniflare/src/plugins/pipelines/index.ts @@ -1,6 +1,7 @@ import SCRIPT_PIPELINE_OBJECT from "worker:pipelines/pipeline"; import { z } from "zod"; import { + buildRemoteProxyProps, namespaceKeys, ProxyNodeBinding, remoteProxyClientWorker, @@ -36,16 +37,24 @@ export const PipelineOptionsSchema = z.object({ export const PIPELINES_PLUGIN_NAME = "pipelines"; const SERVICE_PIPELINE_PREFIX = `${PIPELINES_PLUGIN_NAME}:pipeline`; +const PIPELINES_REMOTE_SERVICE_NAME = `${PIPELINES_PLUGIN_NAME}:pipeline:remote`; export const PIPELINE_PLUGIN: Plugin = { options: PipelineOptionsSchema, bindingTypeDescription: "Pipeline", getBindings(options) { const pipelines = bindingEntries(options.pipelines); - return pipelines.map(([name, { id }]) => ({ - name, - service: { name: `${SERVICE_PIPELINE_PREFIX}:${id}` }, - })); + return pipelines.map( + ([name, { id, remoteProxyConnectionString }]) => ({ + name, + service: remoteProxyConnectionString + ? { + name: PIPELINES_REMOTE_SERVICE_NAME, + props: buildRemoteProxyProps(remoteProxyConnectionString, name), + } + : { name: `${SERVICE_PIPELINE_PREFIX}:${id}` }, + }) + ); }, getNodeBindings(options) { const buckets = namespaceKeys(options.pipelines); @@ -56,24 +65,31 @@ export const PIPELINE_PLUGIN: Plugin = { async getServices({ options }) { const pipelines = bindingEntries(options.pipelines); - const services = []; - for (const [bindingName, pipeline] of pipelines) { + const services: Service[] = []; + let hasRemote = false; + for (const [, pipeline] of pipelines) { + if (pipeline.remoteProxyConnectionString) { + hasRemote = true; + continue; + } services.push({ name: `${SERVICE_PIPELINE_PREFIX}:${pipeline.id}`, - worker: pipeline.remoteProxyConnectionString - ? remoteProxyClientWorker( - pipeline.remoteProxyConnectionString, - bindingName - ) - : { - compatibilityDate: "2024-12-30", - modules: [ - { - name: "pipeline.worker.js", - esModule: SCRIPT_PIPELINE_OBJECT(), - }, - ], + worker: { + compatibilityDate: "2024-12-30", + modules: [ + { + name: "pipeline.worker.js", + esModule: SCRIPT_PIPELINE_OBJECT(), }, + ], + }, + }); + } + + if (hasRemote) { + services.push({ + name: PIPELINES_REMOTE_SERVICE_NAME, + worker: remoteProxyClientWorker(), }); } diff --git a/packages/miniflare/src/plugins/r2/index.ts b/packages/miniflare/src/plugins/r2/index.ts index f089c2917f..59543f5d40 100644 --- a/packages/miniflare/src/plugins/r2/index.ts +++ b/packages/miniflare/src/plugins/r2/index.ts @@ -4,6 +4,7 @@ import SCRIPT_R2_PUBLIC from "worker:r2/public"; import { z } from "zod"; import { SharedBindings } from "../../workers"; import { + buildRemoteProxyProps, getMiniflareObjectBindings, getPersistPath, getUserBindingServiceName, @@ -48,6 +49,8 @@ export const R2SharedOptionsSchema = z.object({ export const R2_PLUGIN_NAME = "r2"; const R2_STORAGE_SERVICE_NAME = `${R2_PLUGIN_NAME}:storage`; const R2_BUCKET_SERVICE_PREFIX = `${R2_PLUGIN_NAME}:bucket`; +// One shared remote-proxy service for all remote R2 buckets (config via props). +const R2_REMOTE_SERVICE_NAME = `${R2_PLUGIN_NAME}:bucket:remote`; export const R2_PUBLIC_SERVICE_NAME = `${R2_PLUGIN_NAME}:public`; const R2_BUCKET_OBJECT_CLASS_NAME = "R2BucketObject"; const R2_BUCKET_OBJECT: Worker_Binding_DurableObjectNamespaceDesignator = { @@ -97,13 +100,20 @@ export const R2_PLUGIN: Plugin< const buckets = namespaceEntries(options.r2Buckets); return buckets.map(([name, bucket]) => ({ name, - r2Bucket: { - name: getUserBindingServiceName( - R2_BUCKET_SERVICE_PREFIX, - bucket.id, - bucket.remoteProxyConnectionString - ), - }, + r2Bucket: bucket.remoteProxyConnectionString + ? { + name: R2_REMOTE_SERVICE_NAME, + props: buildRemoteProxyProps( + bucket.remoteProxyConnectionString, + name + ), + } + : { + name: getUserBindingServiceName( + R2_BUCKET_SERVICE_PREFIX, + bucket.id + ), + }, })); }, getNodeBindings(options) { @@ -122,20 +132,28 @@ export const R2_PLUGIN: Plugin< }) { const persist = sharedOptions.r2Persist; const buckets = namespaceEntries(options.r2Buckets); - const services = buckets.map( - ([name, { id, remoteProxyConnectionString }]) => ({ - name: getUserBindingServiceName( - R2_BUCKET_SERVICE_PREFIX, - id, - remoteProxyConnectionString - ), - worker: remoteProxyConnectionString - ? remoteProxyClientWorker(remoteProxyConnectionString, name) - : objectEntryWorker(R2_BUCKET_OBJECT, id), - }) - ); - if (buckets.length > 0) { + const services: Service[] = []; + let hasRemote = false; + for (const [, { id, remoteProxyConnectionString }] of buckets) { + if (remoteProxyConnectionString) { + hasRemote = true; + } else { + services.push({ + name: getUserBindingServiceName(R2_BUCKET_SERVICE_PREFIX, id), + worker: objectEntryWorker(R2_BUCKET_OBJECT, id), + }); + } + } + if (hasRemote) { + services.push({ + name: R2_REMOTE_SERVICE_NAME, + worker: remoteProxyClientWorker(), + }); + } + + const hasLocal = services.some((s) => s.name !== R2_REMOTE_SERVICE_NAME); + if (hasLocal) { const uniqueKey = `miniflare-${R2_BUCKET_OBJECT_CLASS_NAME}`; const persistPath = getPersistPath( R2_PLUGIN_NAME, @@ -183,8 +201,11 @@ export const R2_PLUGIN: Plugin< }; services.push(storageService, objectService); - for (const bucket of buckets) { - await migrateDatabase(log, uniqueKey, persistPath, bucket[1].id); + for (const [, bucket] of buckets) { + if (bucket.remoteProxyConnectionString) { + continue; + } + await migrateDatabase(log, uniqueKey, persistPath, bucket.id); } } diff --git a/packages/miniflare/src/plugins/shared/constants.ts b/packages/miniflare/src/plugins/shared/constants.ts index b26fcc06a4..02c5c2ef60 100644 --- a/packages/miniflare/src/plugins/shared/constants.ts +++ b/packages/miniflare/src/plugins/shared/constants.ts @@ -61,7 +61,11 @@ export function _enableControlEndpoints() { export function objectEntryWorker( durableObjectNamespace: Worker_Binding_DurableObjectNamespaceDesignator, - namespace: string + // When provided, the namespace is baked into the worker as a static binding + // (the original per-resource model). When omitted, the namespace is supplied + // per-request via `ctx.props` (the props-based model that lets a single entry + // service serve any number of namespaces). + namespace?: string ): Worker { return { compatibilityDate: "2023-07-24", @@ -69,7 +73,9 @@ export function objectEntryWorker( { name: "object-entry.worker.js", esModule: SCRIPT_OBJECT_ENTRY() }, ], bindings: [ - { name: SharedBindings.TEXT_NAMESPACE, text: namespace }, + ...(namespace !== undefined + ? [{ name: SharedBindings.TEXT_NAMESPACE, text: namespace }] + : []), { name: SharedBindings.DURABLE_OBJECT_NAMESPACE_OBJECT, durableObjectNamespace, @@ -78,12 +84,13 @@ export function objectEntryWorker( }; } -export function remoteProxyClientWorker( - remoteProxyConnectionString: RemoteProxyConnectionString | undefined, - binding: string, - script?: () => string -) { - const cfTraceId = process.env.CF_TRACE_ID; +// A single remote-proxy client service can serve any number of remote bindings: +// the per-binding data (connection string, binding name, trace id) is supplied +// at runtime via `ctx.props` (see `buildRemoteProxyProps`), rather than baked +// into a per-binding service. The only static, non-props-able binding is the +// loopback service (used to surface diagnostics back to the Miniflare host, +// e.g. a Cloudflare Access block detected on the remote proxy response). +export function remoteProxyClientWorker(script?: () => string) { return { compatibilityDate: "2025-01-01", modules: [ @@ -92,32 +99,22 @@ export function remoteProxyClientWorker( esModule: (script ?? SCRIPT_REMOTE_PROXY_CLIENT)(), }, ], - bindings: [ - ...(remoteProxyConnectionString?.href - ? [ - { - name: "remoteProxyConnectionString", - text: remoteProxyConnectionString.href, - }, - ] - : []), - { - name: "binding", - text: binding, - }, - ...(cfTraceId - ? [ - { - name: "cfTraceId", - text: cfTraceId, - }, - ] - : []), - // Loopback binding so the proxy client can report diagnostics - // (e.g. a Cloudflare Access block on the remote proxy server) - // back to the Miniflare host for a single, actionable warning. - WORKER_BINDING_SERVICE_LOOPBACK, - ], + bindings: [WORKER_BINDING_SERVICE_LOOPBACK], + }; +} + +// Builds the `props` value for a binding that points at a shared remote-proxy +// client service. Read back in `remote-proxy-client.worker.ts` via `ctx.props`. +export function buildRemoteProxyProps( + remoteProxyConnectionString: RemoteProxyConnectionString | undefined, + binding: string +): { json: string } { + return { + json: JSON.stringify({ + remoteProxyConnectionString: remoteProxyConnectionString?.href, + binding, + cfTraceId: process.env.CF_TRACE_ID, + }), }; } diff --git a/packages/miniflare/src/plugins/stream/index.ts b/packages/miniflare/src/plugins/stream/index.ts index 9c1b446f64..fe75ffbc36 100644 --- a/packages/miniflare/src/plugins/stream/index.ts +++ b/packages/miniflare/src/plugins/stream/index.ts @@ -4,6 +4,7 @@ import OBJECT_SCRIPT from "worker:stream/object"; import { z } from "zod"; import { SharedBindings } from "../../workers"; import { + buildRemoteProxyProps, getMiniflareObjectBindings, getPersistPath, getUserBindingServiceName, @@ -31,6 +32,7 @@ export const StreamSharedOptionsSchema = z.object({ }); export const STREAM_PLUGIN_NAME = "stream"; +const STREAM_REMOTE_SERVICE_NAME = `${STREAM_PLUGIN_NAME}:remote`; const STREAM_STORAGE_SERVICE_NAME = `${STREAM_PLUGIN_NAME}:storage`; const STREAM_OBJECT_SERVICE_NAME = `${STREAM_PLUGIN_NAME}:object`; export const STREAM_OBJECT_CLASS_NAME = "StreamObject"; @@ -52,16 +54,18 @@ export const STREAM_PLUGIN: Plugin< return [ { name: options.stream.binding, - service: { - name: getUserBindingServiceName( - STREAM_PLUGIN_NAME, - "service", - options.stream.remoteProxyConnectionString - ), - entrypoint: options.stream.remoteProxyConnectionString - ? undefined - : "StreamBinding", - }, + service: options.stream.remoteProxyConnectionString + ? { + name: STREAM_REMOTE_SERVICE_NAME, + props: buildRemoteProxyProps( + options.stream.remoteProxyConnectionString, + options.stream.binding + ), + } + : { + name: getUserBindingServiceName(STREAM_PLUGIN_NAME, "service"), + entrypoint: "StreamBinding", + }, }, ]; }, @@ -85,19 +89,10 @@ export const STREAM_PLUGIN: Plugin< } if (options.stream.remoteProxyConnectionString) { - const serviceName = getUserBindingServiceName( - STREAM_PLUGIN_NAME, - "service", - options.stream.remoteProxyConnectionString - ); - return [ { - name: serviceName, - worker: remoteProxyClientWorker( - options.stream.remoteProxyConnectionString, - options.stream.binding - ), + name: STREAM_REMOTE_SERVICE_NAME, + worker: remoteProxyClientWorker(), }, ]; } diff --git a/packages/miniflare/src/plugins/vectorize/index.ts b/packages/miniflare/src/plugins/vectorize/index.ts index ce9a0be971..b9e4f58528 100644 --- a/packages/miniflare/src/plugins/vectorize/index.ts +++ b/packages/miniflare/src/plugins/vectorize/index.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { - getUserBindingServiceName, + buildRemoteProxyProps, ProxyNodeBinding, remoteProxyClientWorker, } from "../shared"; @@ -18,6 +18,7 @@ export const VectorizeOptionsSchema = z.object({ }); export const VECTORIZE_PLUGIN_NAME = "vectorize"; +const VECTORIZE_REMOTE_SERVICE_NAME = `${VECTORIZE_PLUGIN_NAME}:remote`; export const VECTORIZE_PLUGIN: Plugin = { options: VectorizeOptionsSchema, @@ -37,10 +38,10 @@ export const VECTORIZE_PLUGIN: Plugin = { { name: "fetcher", service: { - name: getUserBindingServiceName( - VECTORIZE_PLUGIN_NAME, - name, - remoteProxyConnectionString + name: VECTORIZE_REMOTE_SERVICE_NAME, + props: buildRemoteProxyProps( + remoteProxyConnectionString, + name ), }, }, @@ -74,21 +75,15 @@ export const VECTORIZE_PLUGIN: Plugin = { ); }, async getServices({ options }) { - if (!options.vectorize) { + if (!options.vectorize || Object.keys(options.vectorize).length === 0) { return []; } - return Object.entries(options.vectorize).map( - ([name, { remoteProxyConnectionString }]) => { - return { - name: getUserBindingServiceName( - VECTORIZE_PLUGIN_NAME, - name, - remoteProxyConnectionString - ), - worker: remoteProxyClientWorker(remoteProxyConnectionString, name), - }; - } - ); + return [ + { + name: VECTORIZE_REMOTE_SERVICE_NAME, + worker: remoteProxyClientWorker(), + }, + ]; }, }; diff --git a/packages/miniflare/src/plugins/vpc-networks/index.ts b/packages/miniflare/src/plugins/vpc-networks/index.ts index 9314fb1609..bc363f2bba 100644 --- a/packages/miniflare/src/plugins/vpc-networks/index.ts +++ b/packages/miniflare/src/plugins/vpc-networks/index.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { - getUserBindingServiceName, + buildRemoteProxyProps, ProxyNodeBinding, remoteProxyClientWorker, } from "../shared"; @@ -26,6 +26,7 @@ export const VpcNetworksOptionsSchema = z.object({ }); export const VPC_NETWORKS_PLUGIN_NAME = "vpc-networks"; +const VPC_NETWORKS_REMOTE_SERVICE_NAME = `${VPC_NETWORKS_PLUGIN_NAME}:remote`; export const VPC_NETWORKS_PLUGIN: Plugin = { options: VpcNetworksOptionsSchema, @@ -36,16 +37,14 @@ export const VPC_NETWORKS_PLUGIN: Plugin = { } return Object.entries(options.vpcNetworks).map(([name, binding]) => { - const identifier = - "tunnel_id" in binding ? binding.tunnel_id : binding.network_id; return { name, service: { - name: getUserBindingServiceName( - VPC_NETWORKS_PLUGIN_NAME, - identifier, - binding.remoteProxyConnectionString + name: VPC_NETWORKS_REMOTE_SERVICE_NAME, + props: buildRemoteProxyProps( + binding.remoteProxyConnectionString, + name ), }, }; @@ -63,24 +62,15 @@ export const VPC_NETWORKS_PLUGIN: Plugin = { ); }, async getServices({ options }) { - if (!options.vpcNetworks) { + if (!options.vpcNetworks || Object.keys(options.vpcNetworks).length === 0) { return []; } - return Object.entries(options.vpcNetworks).map(([name, binding]) => { - const identifier = - "tunnel_id" in binding ? binding.tunnel_id : binding.network_id; - return { - name: getUserBindingServiceName( - VPC_NETWORKS_PLUGIN_NAME, - identifier, - binding.remoteProxyConnectionString - ), - worker: remoteProxyClientWorker( - binding.remoteProxyConnectionString, - name - ), - }; - }); + return [ + { + name: VPC_NETWORKS_REMOTE_SERVICE_NAME, + worker: remoteProxyClientWorker(), + }, + ]; }, }; diff --git a/packages/miniflare/src/plugins/vpc-services/index.ts b/packages/miniflare/src/plugins/vpc-services/index.ts index 0bdd14be54..597155d4f6 100644 --- a/packages/miniflare/src/plugins/vpc-services/index.ts +++ b/packages/miniflare/src/plugins/vpc-services/index.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { - getUserBindingServiceName, + buildRemoteProxyProps, ProxyNodeBinding, remoteProxyClientWorker, } from "../shared"; @@ -18,6 +18,7 @@ export const VpcServicesOptionsSchema = z.object({ }); export const VPC_SERVICES_PLUGIN_NAME = "vpc-services"; +const VPC_SERVICES_REMOTE_SERVICE_NAME = `${VPC_SERVICES_PLUGIN_NAME}:remote`; export const VPC_SERVICES_PLUGIN: Plugin = { options: VpcServicesOptionsSchema, @@ -28,16 +29,13 @@ export const VPC_SERVICES_PLUGIN: Plugin = { } return Object.entries(options.vpcServices).map( - ([name, { service_id, remoteProxyConnectionString }]) => { + ([name, { remoteProxyConnectionString }]) => { return { name, service: { - name: getUserBindingServiceName( - VPC_SERVICES_PLUGIN_NAME, - service_id, - remoteProxyConnectionString - ), + name: VPC_SERVICES_REMOTE_SERVICE_NAME, + props: buildRemoteProxyProps(remoteProxyConnectionString, name), }, }; } @@ -55,21 +53,15 @@ export const VPC_SERVICES_PLUGIN: Plugin = { ); }, async getServices({ options }) { - if (!options.vpcServices) { + if (!options.vpcServices || Object.keys(options.vpcServices).length === 0) { return []; } - return Object.entries(options.vpcServices).map( - ([name, { service_id, remoteProxyConnectionString }]) => { - return { - name: getUserBindingServiceName( - VPC_SERVICES_PLUGIN_NAME, - service_id, - remoteProxyConnectionString - ), - worker: remoteProxyClientWorker(remoteProxyConnectionString, name), - }; - } - ); + return [ + { + name: VPC_SERVICES_REMOTE_SERVICE_NAME, + worker: remoteProxyClientWorker(), + }, + ]; }, }; diff --git a/packages/miniflare/src/plugins/websearch/index.ts b/packages/miniflare/src/plugins/websearch/index.ts index a8271680ca..70e29380cf 100644 --- a/packages/miniflare/src/plugins/websearch/index.ts +++ b/packages/miniflare/src/plugins/websearch/index.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { - getUserBindingServiceName, + buildRemoteProxyProps, ProxyNodeBinding, remoteProxyClientWorker, } from "../shared"; @@ -19,6 +19,7 @@ export const WebsearchOptionsSchema = z.object({ export const WEBSEARCH_PLUGIN_NAME = "websearch"; const WEBSEARCH_SCOPE = "websearch"; +const WEBSEARCH_REMOTE_SERVICE_NAME = `${WEBSEARCH_SCOPE}:remote`; export const WEBSEARCH_PLUGIN: Plugin = { options: WebsearchOptionsSchema, @@ -26,7 +27,7 @@ export const WEBSEARCH_PLUGIN: Plugin = { async getBindings(options) { const bindings: { name: string; - service: { name: string }; + service: { name: string; props?: { json: string } }; }[] = []; for (const [bindingName, entry] of Object.entries( @@ -35,10 +36,10 @@ export const WEBSEARCH_PLUGIN: Plugin = { bindings.push({ name: bindingName, service: { - name: getUserBindingServiceName( - WEBSEARCH_SCOPE, - bindingName, - entry.remoteProxyConnectionString + name: WEBSEARCH_REMOTE_SERVICE_NAME, + props: buildRemoteProxyProps( + entry.remoteProxyConnectionString, + bindingName ), }, }); @@ -61,19 +62,10 @@ export const WEBSEARCH_PLUGIN: Plugin = { worker: ReturnType; }[] = []; - for (const [bindingName, entry] of Object.entries( - options.websearch ?? {} - )) { + if (Object.keys(options.websearch ?? {}).length > 0) { services.push({ - name: getUserBindingServiceName( - WEBSEARCH_SCOPE, - bindingName, - entry.remoteProxyConnectionString - ), - worker: remoteProxyClientWorker( - entry.remoteProxyConnectionString, - bindingName - ), + name: WEBSEARCH_REMOTE_SERVICE_NAME, + worker: remoteProxyClientWorker(), }); } diff --git a/packages/miniflare/src/workers/dispatch-namespace/dispatch-namespace-proxy.worker.ts b/packages/miniflare/src/workers/dispatch-namespace/dispatch-namespace-proxy.worker.ts index 80fdc81db8..fa5faeb250 100644 --- a/packages/miniflare/src/workers/dispatch-namespace/dispatch-namespace-proxy.worker.ts +++ b/packages/miniflare/src/workers/dispatch-namespace/dispatch-namespace-proxy.worker.ts @@ -4,21 +4,27 @@ import { makeRemoteProxyStub, throwRemoteRequired, } from "../shared/remote-bindings-utils"; -import type { RemoteBindingEnv } from "../shared/remote-bindings-utils"; +import type { + RemoteBindingEnv, + RemoteBindingProps, +} from "../shared/remote-bindings-utils"; /** Proxy client for dispatch namespace bindings. */ -export default class DispatchNamespaceProxy extends WorkerEntrypoint { +export default class DispatchNamespaceProxy extends WorkerEntrypoint< + RemoteBindingEnv, + RemoteBindingProps +> { get( name: string, args?: { [key: string]: unknown }, options?: DynamicDispatchOptions ): Fetcher { - if (!this.env.remoteProxyConnectionString) { - throwRemoteRequired(this.env.binding); + if (!this.ctx.props.remoteProxyConnectionString) { + throwRemoteRequired(this.ctx.props.binding); } return makeRemoteProxyStub( - this.env.remoteProxyConnectionString, - this.env.binding, + this.ctx.props.remoteProxyConnectionString, + this.ctx.props.binding, { "MF-Dispatch-Namespace-Options": JSON.stringify({ name, @@ -26,7 +32,7 @@ export default class DispatchNamespaceProxy extends WorkerEntrypoint>{ - async fetch(request, env) { - const name = env[SharedBindings.TEXT_NAMESPACE]; +export default >{ + async fetch(request, env, ctx) { + // Prefer the namespace passed at runtime via `ctx.props` (props-based + // model: one entry service serves many namespaces). Fall back to the + // static binding for callers that still bake the namespace in. + const name = + ctx.props[SharedBindings.TEXT_NAMESPACE] ?? + env[SharedBindings.TEXT_NAMESPACE]; + if (name === undefined) { + throw new Error( + "object-entry worker: no namespace provided via props or binding" + ); + } const objectNamespace = env[SharedBindings.DURABLE_OBJECT_NAMESPACE_OBJECT]; const id = objectNamespace.idFromName(name); const stub = objectNamespace.get(id); diff --git a/packages/miniflare/src/workers/shared/remote-bindings-utils.ts b/packages/miniflare/src/workers/shared/remote-bindings-utils.ts index 7034bb3be3..fcf19a22d8 100644 --- a/packages/miniflare/src/workers/shared/remote-bindings-utils.ts +++ b/packages/miniflare/src/workers/shared/remote-bindings-utils.ts @@ -2,18 +2,27 @@ import { newWebSocketRpcSession } from "capnweb"; import type { SharedBindings } from "./constants"; /** - * Common environment type for remote binding workers. + * Common environment type for remote binding workers. The loopback service is + * the only binding still passed via env (services can't travel through props); + * the per-binding fields now arrive via `ctx.props` (see `RemoteBindingProps`). */ export type RemoteBindingEnv = { - remoteProxyConnectionString?: string; - binding: string; - cfTraceId?: string; // Optional loopback service used to surface diagnostics back to the // Miniflare host (e.g. a Cloudflare Access block detected on the response // from the remote-bindings proxy server). [SharedBindings.MAYBE_SERVICE_LOOPBACK]?: Fetcher; }; +/** + * Per-binding configuration supplied at runtime via `ctx.props`. This is what + * lets a single remote-proxy client service serve many bindings. + */ +export type RemoteBindingProps = { + remoteProxyConnectionString?: string; + binding: string; + cfTraceId?: string; +}; + /** Headers sent alongside proxy requests to provide additional context. */ export type ProxyMetadata = { "MF-Dispatch-Namespace-Options"?: string; diff --git a/packages/miniflare/src/workers/shared/remote-proxy-client.worker.ts b/packages/miniflare/src/workers/shared/remote-proxy-client.worker.ts index 2a7dbb67ff..a4090913d8 100644 --- a/packages/miniflare/src/workers/shared/remote-proxy-client.worker.ts +++ b/packages/miniflare/src/workers/shared/remote-proxy-client.worker.ts @@ -5,29 +5,38 @@ import { makeRemoteProxyStub, throwRemoteRequired, } from "./remote-bindings-utils"; -import type { RemoteBindingEnv } from "./remote-bindings-utils"; +import type { + RemoteBindingEnv, + RemoteBindingProps, +} from "./remote-bindings-utils"; /** Generic remote proxy client for bindings. */ -export default class Client extends WorkerEntrypoint { +export default class Client extends WorkerEntrypoint< + RemoteBindingEnv, + RemoteBindingProps +> { fetch(request: Request): Promise { return makeFetch( - this.env.remoteProxyConnectionString, - this.env.binding, + this.ctx.props.remoteProxyConnectionString, + this.ctx.props.binding, undefined, - this.env.cfTraceId, + this.ctx.props.cfTraceId, this.env[SharedBindings.MAYBE_SERVICE_LOOPBACK] )(request); } - constructor(ctx: ExecutionContext, env: RemoteBindingEnv) { + constructor( + ctx: ExecutionContext, + env: RemoteBindingEnv + ) { super(ctx, env); - const stub = env.remoteProxyConnectionString + const stub = ctx.props.remoteProxyConnectionString ? makeRemoteProxyStub( - env.remoteProxyConnectionString, - env.binding, + ctx.props.remoteProxyConnectionString, + ctx.props.binding, undefined, - env.cfTraceId, + ctx.props.cfTraceId, env[SharedBindings.MAYBE_SERVICE_LOOPBACK] ) : undefined; @@ -38,7 +47,7 @@ export default class Client extends WorkerEntrypoint { return Reflect.get(target, prop); } if (!stub) { - throwRemoteRequired(env.binding); + throwRemoteRequired(ctx.props.binding); } return Reflect.get(stub, prop); }, diff --git a/packages/miniflare/test/plugins/local-explorer/binding-map.spec.ts b/packages/miniflare/test/plugins/local-explorer/binding-map.spec.ts new file mode 100644 index 0000000000..1ebf8b24f5 --- /dev/null +++ b/packages/miniflare/test/plugins/local-explorer/binding-map.spec.ts @@ -0,0 +1,89 @@ +import { Miniflare } from "miniflare"; +import { afterAll, beforeAll, describe, test } from "vitest"; +import { CorePaths } from "../../../src/workers/core/constants"; +import { disposeWithRetry } from "../../test-shared"; +import type { RemoteProxyConnectionString } from "miniflare"; + +const BASE_URL = `http://localhost${CorePaths.EXPLORER}/api`; + +// A dummy remote-proxy endpoint. Listing resources reads only the binding map, +// so this is never actually contacted. +const remoteProxyConnectionString = new URL( + "http://127.0.0.1:9999/" +) as unknown as RemoteProxyConnectionString; + +// Remote KV/R2/D1 bindings all share one proxy service (`kv:ns:remote`, +// `r2:bucket:remote`, `d1:db:remote`). Remote resources aren't supported in the +// local explorer, so they must be skipped rather than collapse to the literal id +// "remote" (a collision that would also leak a bogus "remote" entry). Local +// resources alongside them must still be surfaced. +describe("Local Explorer remote binding skipping", () => { + let mf: Miniflare; + + beforeAll(async () => { + mf = new Miniflare({ + inspectorPort: 0, + compatibilityDate: "2025-01-01", + modules: true, + script: `export default { fetch() { return new Response("user worker"); } }`, + unsafeLocalExplorer: true, + kvNamespaces: { + LOCAL_KV: "kv-local", + REMOTE_KV_A: { id: "kv-a", remoteProxyConnectionString }, + REMOTE_KV_B: { id: "kv-b", remoteProxyConnectionString }, + }, + r2Buckets: { + LOCAL_R2: "r2-local", + REMOTE_R2_A: { id: "r2-a", remoteProxyConnectionString }, + REMOTE_R2_B: { id: "r2-b", remoteProxyConnectionString }, + }, + d1Databases: { + LOCAL_D1: "d1-local", + REMOTE_D1_A: { id: "d1-a", remoteProxyConnectionString }, + REMOTE_D1_B: { id: "d1-b", remoteProxyConnectionString }, + }, + }); + }); + + afterAll(async () => { + await disposeWithRetry(mf); + }); + + test("skips remote KV namespaces, surfacing only local ones", async ({ + expect, + }) => { + const response = await mf.dispatchFetch( + `${BASE_URL}/storage/kv/namespaces` + ); + expect(response.status).toBe(200); + const body = (await response.json()) as { + result: Array<{ id: string }>; + }; + const ids = body.result.map((ns) => ns.id).sort(); + expect(ids).toEqual(["kv-local"]); + }); + + test("skips remote R2 buckets, surfacing only local ones", async ({ + expect, + }) => { + const response = await mf.dispatchFetch(`${BASE_URL}/r2/buckets`); + expect(response.status).toBe(200); + const body = (await response.json()) as { + result: { buckets: Array<{ name: string }> }; + }; + const names = body.result.buckets.map((b) => b.name).sort(); + expect(names).toEqual(["r2-local"]); + }); + + test("skips remote D1 databases, surfacing only local ones", async ({ + expect, + }) => { + const response = await mf.dispatchFetch(`${BASE_URL}/d1/database`); + expect(response.status).toBe(200); + const body = (await response.json()) as { + result: Array<{ uuid: string }>; + }; + const uuids = body.result.map((db) => db.uuid).sort(); + expect(uuids).toEqual(["d1-local"]); + }); +});