S
SolidJS•3w ago
dylanj

How to manually invoke server functions?

Hey all, have a bit of a weird problem trying to combine SolidStart with Cloudflare Durable Objects and Server Functions. The following works great, unless the server function is used during server side rendering, in which case the "request" context that we forward to the durable object is for the page being rendered, not for the invocation of the server function. So the durable object responds with the fully rendered page, instead of the server function result. As you can imagine, this causes issues. It's easy enough to detect when this happens, but I can't figure out how to construct a new request to send to the Durable Object to invoke just the server function? Or perhaps it would be best to skip any rendering on the worker entirely, and just send the whole request to the durable object to begin with. However this would require me to fork the cloudflare-durable preset.
export const increment = async (gameId: string) => {
"use server";
const event = getRequestEvent()!;
const cloudflare = event.nativeEvent.context.cloudflare;

// We're in the worker not the durable object
if (!cloudflare.durable) {
const binding = cloudflare.env.GAME;
const id = binding.idFromName(gameId);

return (await binding.get(id).fetch(event.request)) as number;
}

let value: number = (await cloudflare.context.storage.get("counter")) || 0;

value += 1;
await cloudflare.context.storage.put("counter", value);

return value;
};
export const increment = async (gameId: string) => {
"use server";
const event = getRequestEvent()!;
const cloudflare = event.nativeEvent.context.cloudflare;

// We're in the worker not the durable object
if (!cloudflare.durable) {
const binding = cloudflare.env.GAME;
const id = binding.idFromName(gameId);

return (await binding.get(id).fetch(event.request)) as number;
}

let value: number = (await cloudflare.context.storage.get("counter")) || 0;

value += 1;
await cloudflare.context.storage.put("counter", value);

return value;
};
1 Reply
dylanj
dylanjOP•2w ago
Is the RPC interface of server functions considered "private"? If so, is there some directive to generate a copy of the "client" version of the server function on the server, so I can use that to construct the request to send to the durable object? I'm probably going to need to fork the cloudflare-durable preset anyway because it hard codes in the durable object name for websockets which doesn't work for me use case, so perhaps I've answered my own question and the worker should just always forward to the durable object for rendering. Although I have some routes that aren't paramaterised by the GameID, so they'll have to remain in the worker 🤔 In addition to that, is there any way to hook into the kind of conditional compilation that is used to drive the magic behind server functions? i.e. generate this code on the client and this code on the server? I’m porting over an app from using Leptos / Rust which has a similar server function thing, but it’s much easier to make your own variation on that tech using Rusts compiler config / conditional compilation Actually turns out the root of the problem is a kind of "double encoding" problem, the response from the durable object is already the encoded server function response, e.g.
;0x0000002c;((self.$R=self.$R||{})["server-fn:0"]=[],46)
;0x0000002c;((self.$R=self.$R||{})["server-fn:0"]=[],46)
so returning that response from inside the worker's server function results in it being "double encoded" in a way. I'd need to find a way to bypass the encoding from the worker's server function and return the response directly 🤔 Alright I was able to solve the double encoding problem with X-Content-Raw
export const delegate = async (gameId: string): Promise<any> => {
const event = getRequestEvent()!;
const cloudflare = event.nativeEvent.context.cloudflare;

const binding = cloudflare.env.GAME;
const id = binding.idFromName(gameId);

const response = (await binding.get(id).fetch(event.request)) as Response;

const proxyResponse = new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: {
...Object.fromEntries(response.headers.entries()),
"X-Content-Raw": "true",
},
});

return proxyResponse;
};
export const delegate = async (gameId: string): Promise<any> => {
const event = getRequestEvent()!;
const cloudflare = event.nativeEvent.context.cloudflare;

const binding = cloudflare.env.GAME;
const id = binding.idFromName(gameId);

const response = (await binding.get(id).fetch(event.request)) as Response;

const proxyResponse = new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: {
...Object.fromEntries(response.headers.entries()),
"X-Content-Raw": "true",
},
});

return proxyResponse;
};
but still have the "rendering the whole page during ssr" problem since I need to figure out how to manually invoke the server function I was able to reverse engineer most of the server side function protocol, but it seemed rather fragile to rely upon. In particular I found it difficult to abstract out routing calls to the Durable Object with higher order functions and "use server" interacting weirdly. I've gone back to just using regular API endpoints which means I unfortunately loose the nice DX of server functions, and then have a middleware intercept those calls and forward them to the DO which is injected into the request context via the entrypoint
export const POST = async ({ params }: APIEvent) => {
const storage = getStorage();

let value = (await storage.get<number>("counter")) || 0;
value += 1;
await storage.put("counter", value);

return value;
};
export const POST = async ({ params }: APIEvent) => {
const storage = getStorage();

let value = (await storage.get<number>("counter")) || 0;
value += 1;
await storage.put("counter", value);

return value;
};
export default createMiddleware({
onRequest: async (event): Promise<any> => {
if (event.nativeEvent.context?.cloudflare?.durable) {
return;
}

const url = new URL(event.request.url);

const match = url.pathname.match(
/^\/api\/game\/by_code\/(?<code>[a-zA-Z0-9]+)\//,
);
if (match === null) {
return;
}

const code = match.groups!.code;

return event.nativeEvent.context.cloudflare.env.GAME.getByName(code).fetch(
event.request as any,
);
},
});
export default createMiddleware({
onRequest: async (event): Promise<any> => {
if (event.nativeEvent.context?.cloudflare?.durable) {
return;
}

const url = new URL(event.request.url);

const match = url.pathname.match(
/^\/api\/game\/by_code\/(?<code>[a-zA-Z0-9]+)\//,
);
if (match === null) {
return;
}

const code = match.groups!.code;

return event.nativeEvent.context.cloudflare.env.GAME.getByName(code).fetch(
event.request as any,
);
},
});
I then also need a SSR compatible fetch becuase you can't use relative URLs during SSR
export const ssrFetch = async (url: string, options?: RequestInit) => {
const event = getRequestEvent();
// Client side, proceed as normal
if (!event) {
return fetch(url, options).then((it) => it.json());
}

// Might be worth sending this directly to the Durable Object instead
// That would help avoid the overhead of an additional API request
const baseUrl = new URL(event.request.url).origin;
const fullUrl = new URL(url, baseUrl).toString();

return fetch(fullUrl, options).then((it) => it.json());
};
export const ssrFetch = async (url: string, options?: RequestInit) => {
const event = getRequestEvent();
// Client side, proceed as normal
if (!event) {
return fetch(url, options).then((it) => it.json());
}

// Might be worth sending this directly to the Durable Object instead
// That would help avoid the overhead of an additional API request
const baseUrl = new URL(event.request.url).origin;
const fullUrl = new URL(url, baseUrl).toString();

return fetch(fullUrl, options).then((it) => it.json());
};
Alright, I was able to get server functions working with durable objects using a kind of janking middleware, unfortunately the middleware has to exists inside the server function definition so it can get access to the server function ID which is set by a proxy
export const increment = (gameID: string) => {
"use server";
return durableServerFn(gameID, async () => {
const storage = getStorage();

let value = (await storage.get<number>("counter")) || 0;
value += 1;
await storage.put("counter", value);

return value;
});
};
export const increment = (gameID: string) => {
"use server";
return durableServerFn(gameID, async () => {
const storage = getStorage();

let value = (await storage.get<number>("counter")) || 0;
value += 1;
await storage.put("counter", value);

return value;
});
};
I think I'm just going to go with a regular API route though because I don't like that there's no real spec to the server functions and I don't want it breaking between deployments / unable to handle proper versioning

Did you find this page helpful?