Skip to content

Instantly share code, notes, and snippets.

@SinZ163
Last active December 13, 2024 21:02
Show Gist options
  • Select an option

  • Save SinZ163/9835a45aa8d06899c563d4e1ffa2ce24 to your computer and use it in GitHub Desktop.

Select an option

Save SinZ163/9835a45aa8d06899c563d4e1ffa2ce24 to your computer and use it in GitHub Desktop.
Solid websocket poc
import { defineConfig } from "@solidjs/start/config";
const app = defineConfig({
ssr: false,
server: {
experimental: {
websocket: true
}
}
});
app.addRouter({
name: "websocket",
type: "http",
handler: "./src/entry-websocket.ts",
target: "server",
"base": "/_ws"
})
export default app;
function Example() {
const {createWSSignal, dispatch} = useWSContext();
const [test, setText] = createWSSignal("testSignal", 0);
return (
<div>
<h1>Hello {test()}</h1>
<button onClick={() => setText(prev => prev + 1)}>Increment</button>
<button onClick={() => setText(prev => prev - 1)}>Decrement</button>
</div>
);
}
export default function App() {
const {context, wsErrorReason, wsReady} = createWebSocket();
<WSContext.Provider value={context}>
<Show when={wsReady()}>
<Example />
</Show>
</WSContext.Provider>
}
const DataState: Record<string, any> = {};
export default eventHandler({
handler: () => {},
websocket: defineWebSocket({
async open(peer) {
console.log("WebSocket opened");
},
async message(peer, event) {
console.log("WebSocket message", event);
let msg = JSON.parse(event.text()) as WSMessage;
if (msg.type === "register") {
peer.subscribe(msg.id);
if (DataState[msg.id]) {
console.log("Already registered, sending cached value" + DataState[msg.id]);
peer.send({id: msg.id, value: DataState[msg.id]});
}
}
if (msg.type === "change") {
// TODO: Server Validation
DataState[msg.id] = msg.value;
peer.publish(msg.id, msg);
}
},
async close(peer) {
console.log("WebSocket closed");
},
}),
});
import {
createContext,
createEffect,
createSignal,
onCleanup,
Signal,
untrack,
useContext,
} from "solid-js";
import { getReasonForWebSocketClose } from "./server-actions";
import { EventMessage, StartEventMessage } from "./common/message";
interface WSContextType {
ws: WebSocket;
dispatch: <T extends Omit<EventMessage | StartEventMessage, "type">>(
event: T
) => void;
createWSSignal: <T,>(
identifier: string,
defaultValue: T
) => Signal<T>;
}
export const WSContext = createContext<WSContextType>();
export const useWSContext = () => {
const context = useContext(WSContext);
if (!context) {
throw new Error("Missing WSContext");
}
return context;
}
export const createWebSocket = () => {
const [wsErrorReason, setWsErrorReason] = createSignal("");
const [wsReady, setWSReady] = createSignal(false);
const protocol = window.location.protocol === "http:" ? "ws" : "wss";
const ws = new WebSocket(`${protocol}://${window.location.host}/_ws`);
ws.addEventListener("open", () => setWSReady(true));
ws.addEventListener("close", async (e) => {
setWSReady(false);
});
const wsRegistrationCache: Record<string, any> = {};
ws.addEventListener("message", async (ev) => {
let data = ev.data;
if (ev.data instanceof Blob) {
data = await ev.data.text();
}
let msg = JSON.parse(data);
// Cloudflare disconnects websockets if they are idle for ~60-100 seconds
if ("type" in msg && msg["type"] === "ping") {
ws.send(JSON.stringify({ type: "pong" }));
}
if ("id" in msg && "value" in msg) {
wsRegistrationCache[msg.id] = msg.value;
}
});
const createWSSignal = <T,>(
identifier: string,
defaultValue: T
): Signal<T> => {
const [value, setValue] = createSignal(defaultValue);
const [bypass, setBypass] = createSignal(true);
if (!wsRegistrationCache[identifier]) {
ws.send(
JSON.stringify({
type: "register",
id: identifier,
defaultValue,
})
);
} else {
setBypass(true);
setValue(wsRegistrationCache[identifier]);
}
const handleMessage = async (ev: MessageEvent) => {
let data = ev.data;
if (ev.data instanceof Blob) {
data = await ev.data.text();
}
let msg = JSON.parse(data);
if ("id" in msg && "value" in msg) {
if (msg.id === identifier) {
setBypass(true);
setValue(msg.value);
}
}
};
ws.addEventListener("message", handleMessage);
onCleanup(() => {
ws.removeEventListener("message", handleMessage);
});
createEffect(() => {
const val = value();
if (ws.readyState !== ws.OPEN) return;
let bypassVal = false;
untrack(() => {
bypassVal = bypass();
});
if (bypassVal) {
setBypass(false);
return;
}
ws.send(
JSON.stringify({
type: "change",
id: identifier,
value: value(),
})
);
});
return [value, setValue];
};
return {
context: {
ws,
createWSSignal,
},
wsErrorReason,
wsReady,
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment