Skip to content

Commit 654af9f

Browse files
committed
Add Miniflare & Wrangler support for unbound Durable Objects
1 parent 4e49d3e commit 654af9f

File tree

5 files changed

+166
-127
lines changed

5 files changed

+166
-127
lines changed

packages/miniflare/src/index.ts

Lines changed: 44 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import {
4848
HOST_CAPNP_CONNECT,
4949
KV_PLUGIN_NAME,
5050
launchBrowser,
51-
normaliseDurableObject,
51+
normaliseDurableObjects,
5252
PLUGIN_ENTRIES,
5353
Plugins,
5454
PluginServicesOptions,
@@ -336,9 +336,8 @@ function getDurableObjectClassNames(
336336
.flatMap((workerOpts) => {
337337
const workerServiceName = getUserServiceName(workerOpts.core.name);
338338

339-
return Object.values(workerOpts.do.durableObjects ?? {}).map(
340-
(workerDODesignator) => {
341-
const doInfo = normaliseDurableObject(workerDODesignator);
339+
return normaliseDurableObjects(workerOpts.do.durableObjects ?? {}).map(
340+
(doInfo) => {
342341
if (doInfo.serviceName === undefined) {
343342
// Fallback to current worker service if name not defined
344343
doInfo.serviceName = workerServiceName;
@@ -507,26 +506,24 @@ function getExternalServiceEntrypoints(
507506
}
508507

509508
if (workerOpts.do.durableObjects) {
510-
for (const [bindingName, designator] of Object.entries(
511-
workerOpts.do.durableObjects
512-
)) {
513-
const {
514-
className,
515-
scriptName,
516-
unsafePreventEviction,
517-
enableSql: useSQLite,
518-
remoteProxyConnectionString,
519-
} = normaliseDurableObject(designator);
520-
509+
for (const {
510+
binding,
511+
className,
512+
scriptName,
513+
unsafePreventEviction,
514+
enableSql: useSQLite,
515+
remoteProxyConnectionString,
516+
} of normaliseDurableObjects(workerOpts.do.durableObjects)) {
521517
if (
522518
// Skip if it is a remote durable object
523519
remoteProxyConnectionString === undefined &&
524520
// Skip if the durable object is bound to a Worker that exists in the current Miniflare config
525521
scriptName &&
526-
!allWorkerNames.includes(scriptName)
522+
!allWorkerNames.includes(scriptName) &&
523+
binding
527524
) {
528525
// Point it to the outbound do proxy service instead
529-
workerOpts.do.durableObjects[bindingName] = {
526+
const doProxy = {
530527
className: getOutboundDoProxyClassName(scriptName, className),
531528
scriptName: OUTBOUND_DO_PROXY_SERVICE_NAME,
532529
useSQLite,
@@ -538,6 +535,13 @@ function getExternalServiceEntrypoints(
538535
unsafeUniqueKey: `${scriptName}-${className}`,
539536
unsafePreventEviction,
540537
};
538+
if (Array.isArray(workerOpts.do.durableObjects)) {
539+
workerOpts.do.durableObjects = workerOpts.do.durableObjects.map(
540+
(d) => (d.binding === binding ? doProxy : d)
541+
);
542+
} else {
543+
workerOpts.do.durableObjects[binding] = doProxy;
544+
}
541545

542546
const entrypoints = getEntrypoints(scriptName);
543547
entrypoints.classNames.add(className);
@@ -2113,31 +2117,31 @@ export class Miniflare {
21132117
];
21142118
}) ?? []
21152119
);
2116-
const internalObjects = Object.entries(
2120+
2121+
const internalObjects = normaliseDurableObjects(
21172122
workerOpts.do.durableObjects ?? {}
2118-
).reduce<WorkerDefinition["durableObjects"]>(
2119-
(internalObjects, [bindingName, designator]) => {
2120-
const { className, scriptName, remoteProxyConnectionString } =
2121-
normaliseDurableObject(designator);
2122-
2123-
if (
2124-
// If the scriptName is undefined, it defaults to the current worker
2125-
scriptName === undefined ||
2126-
// If the scriptName matches one of the workers defined, it is internal as well
2127-
allWorkerNames.includes(scriptName) ||
2128-
// If it is not a remote durable object
2129-
remoteProxyConnectionString === undefined
2130-
) {
2131-
internalObjects.push({
2132-
name: bindingName,
2133-
className,
2134-
});
2123+
)
2124+
.filter(
2125+
({
2126+
binding,
2127+
className,
2128+
scriptName,
2129+
remoteProxyConnectionString,
2130+
}) => {
2131+
return (
2132+
(scriptName === undefined ||
2133+
// If the scriptName matches one of the workers defined, it is internal as well
2134+
allWorkerNames.includes(scriptName) ||
2135+
// If it is not a remote durable object
2136+
remoteProxyConnectionString === undefined) &&
2137+
binding
2138+
);
21352139
}
2136-
2137-
return internalObjects;
2138-
},
2139-
[]
2140-
);
2140+
)
2141+
.map(({ binding, className }) => ({
2142+
name: binding as string,
2143+
className,
2144+
}));
21412145

21422146
return [
21432147
workerOpts.core.name,

packages/miniflare/src/merge.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ type ArrayRecordKeys<O extends object, K extends keyof O> = K extends unknown
2424
: K
2525
: never;
2626
// "kvNamespaces" | "r2Buckets" | "queueProducers" | "queueConsumers" | ...
27-
type WorkerOptionsArrayRecordKeys = ArrayRecordKeys<
28-
WorkerOptions,
29-
keyof WorkerOptions
27+
type WorkerOptionsArrayRecordKeys = Exclude<
28+
ArrayRecordKeys<WorkerOptions, keyof WorkerOptions>,
29+
"durableObjects"
3030
>;
3131
// Get the record type that can be used for key `K` in `WorkerOptions`
3232
type WorkerOptionsRecord<K extends WorkerOptionsArrayRecordKeys> = Extract<

packages/miniflare/src/plugins/do/index.ts

Lines changed: 74 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -18,42 +18,49 @@ export const DOContainerOptionsSchema = z.object({
1818
});
1919
export type DOContainerOptions = z.infer<typeof DOContainerOptionsSchema>;
2020

21+
const DurableObjectOptions = z.object({
22+
className: z.string(),
23+
scriptName: z.string().optional(),
24+
useSQLite: z.boolean().optional(),
25+
// Allow `uniqueKey` to be customised. We use in Wrangler when setting
26+
// up stub Durable Objects that proxy requests to Durable Objects in
27+
// another `workerd` process, to ensure the IDs created by the stub
28+
// object can be used by the real object too.
29+
unsafeUniqueKey: z
30+
.union([z.string(), z.literal(kUnsafeEphemeralUniqueKey)])
31+
.optional(),
32+
// Prevents the Durable Object being evicted.
33+
unsafePreventEviction: z.boolean().optional(),
34+
remoteProxyConnectionString: z
35+
.custom<RemoteProxyConnectionString>()
36+
.optional(),
37+
container: z.custom<DOContainerOptions>().optional(),
38+
});
39+
2140
export const DurableObjectsOptionsSchema = z.object({
2241
durableObjects: z
23-
.record(
24-
z.union([
25-
z.string(),
26-
z.object({
27-
className: z.string(),
28-
scriptName: z.string().optional(),
29-
useSQLite: z.boolean().optional(),
30-
// Allow `uniqueKey` to be customised. We use in Wrangler when setting
31-
// up stub Durable Objects that proxy requests to Durable Objects in
32-
// another `workerd` process, to ensure the IDs created by the stub
33-
// object can be used by the real object too.
34-
unsafeUniqueKey: z
35-
.union([z.string(), z.literal(kUnsafeEphemeralUniqueKey)])
36-
.optional(),
37-
// Prevents the Durable Object being evicted.
38-
unsafePreventEviction: z.boolean().optional(),
39-
remoteProxyConnectionString: z
40-
.custom<RemoteProxyConnectionString>()
41-
.optional(),
42-
container: z.custom<DOContainerOptions>().optional(),
43-
}),
44-
])
45-
)
42+
.union([
43+
z.array(
44+
DurableObjectOptions.extend({
45+
// What binding name should this DO have? This is optional because not all DOs are configured as bindings
46+
// Some might just be configured via migrations, but should still be allocated storage for e..g ctx.exports support
47+
binding: z.string().optional(),
48+
})
49+
),
50+
z.record(z.union([z.string(), DurableObjectOptions])),
51+
])
4652
.optional(),
4753
});
4854
export const DurableObjectsSharedOptionsSchema = z.object({
4955
durableObjectsPersist: PersistenceSchema,
5056
});
5157

52-
export function normaliseDurableObject(
53-
designator: NonNullable<
58+
export function normaliseDurableObjects(
59+
durableObjects: NonNullable<
5460
z.infer<typeof DurableObjectsOptionsSchema>["durableObjects"]
55-
>[string]
61+
>
5662
): {
63+
binding: string | undefined;
5764
className: string;
5865
scriptName: string | undefined;
5966
serviceName: string | undefined;
@@ -62,33 +69,43 @@ export function normaliseDurableObject(
6269
unsafePreventEviction: boolean | undefined;
6370
remoteProxyConnectionString: RemoteProxyConnectionString | undefined;
6471
container: DOContainerOptions | undefined;
65-
} {
66-
const isObject = typeof designator === "object";
67-
const className = isObject ? designator.className : designator;
68-
const scriptName =
69-
isObject && designator.scriptName !== undefined
70-
? designator.scriptName
72+
}[] {
73+
let normalised: [
74+
string | undefined,
75+
z.infer<typeof DurableObjectOptions> | string,
76+
][] = Array.isArray(durableObjects)
77+
? durableObjects.map((d) => [d.binding, d])
78+
: Object.entries(durableObjects);
79+
80+
return normalised.map(([binding, designator]) => {
81+
const isObject = typeof designator === "object";
82+
const className = isObject ? designator.className : designator;
83+
const scriptName =
84+
isObject && designator.scriptName !== undefined
85+
? designator.scriptName
86+
: undefined;
87+
const serviceName = scriptName ? getUserServiceName(scriptName) : undefined;
88+
const enableSql = isObject ? designator.useSQLite : undefined;
89+
const unsafeUniqueKey = isObject ? designator.unsafeUniqueKey : undefined;
90+
const unsafePreventEviction = isObject
91+
? designator.unsafePreventEviction
92+
: undefined;
93+
const remoteProxyConnectionString = isObject
94+
? designator.remoteProxyConnectionString
7195
: undefined;
72-
const serviceName = scriptName ? getUserServiceName(scriptName) : undefined;
73-
const enableSql = isObject ? designator.useSQLite : undefined;
74-
const unsafeUniqueKey = isObject ? designator.unsafeUniqueKey : undefined;
75-
const unsafePreventEviction = isObject
76-
? designator.unsafePreventEviction
77-
: undefined;
78-
const remoteProxyConnectionString = isObject
79-
? designator.remoteProxyConnectionString
80-
: undefined;
81-
const container = isObject ? designator.container : undefined;
82-
return {
83-
className,
84-
scriptName,
85-
serviceName,
86-
enableSql,
87-
unsafeUniqueKey,
88-
unsafePreventEviction,
89-
remoteProxyConnectionString,
90-
container,
91-
};
96+
const container = isObject ? designator.container : undefined;
97+
return {
98+
binding,
99+
className,
100+
scriptName,
101+
serviceName,
102+
enableSql,
103+
unsafeUniqueKey,
104+
unsafePreventEviction,
105+
remoteProxyConnectionString,
106+
container,
107+
};
108+
});
92109
}
93110

94111
export const DURABLE_OBJECTS_PLUGIN_NAME = "do";
@@ -102,15 +119,14 @@ export const DURABLE_OBJECTS_PLUGIN: Plugin<
102119
options: DurableObjectsOptionsSchema,
103120
sharedOptions: DurableObjectsSharedOptionsSchema,
104121
getBindings(options) {
105-
return Object.entries(options.durableObjects ?? {}).map<Worker_Binding>(
106-
([name, klass]) => {
107-
const { className, serviceName } = normaliseDurableObject(klass);
122+
return normaliseDurableObjects(options.durableObjects ?? {})
123+
.filter((d) => d.binding)
124+
.map<Worker_Binding>(({ binding, className, serviceName }) => {
108125
return {
109-
name,
126+
name: binding,
110127
durableObjectNamespace: { className, serviceName },
111128
};
112-
}
113-
);
129+
});
114130
},
115131
getNodeBindings(options) {
116132
const objects = Object.keys(options.durableObjects ?? {});

packages/wrangler/src/api/integrations/platform/index.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -447,10 +447,6 @@ export function unstable_getMiniflareWorkerOptions(
447447
);
448448
}
449449
if (bindings.durable_objects !== undefined) {
450-
type DurableObjectDefinition = NonNullable<
451-
typeof bindingOptions.durableObjects
452-
>[string];
453-
454450
const classNameToUseSQLite = getClassNamesWhichUseSQLite(config.migrations);
455451

456452
bindingOptions.durableObjects = Object.fromEntries(
@@ -470,7 +466,7 @@ export function unstable_getMiniflareWorkerOptions(
470466
containerBuildId: options?.containerBuildId,
471467
})
472468
: undefined,
473-
} satisfies DurableObjectDefinition,
469+
},
474470
];
475471
})
476472
);

0 commit comments

Comments
 (0)