Compare commits
No commits in common. "a7a7eeac0c371e83abd6f000fce911a5c5f623b5" and "852fa32c93199c91d27259516f2a8d6da7b8c96d" have entirely different histories.
a7a7eeac0c
...
852fa32c93
13 changed files with 190 additions and 599 deletions
|
|
@ -375,15 +375,8 @@ export const relations = defineRelations(
|
||||||
track,
|
track,
|
||||||
trackArtist,
|
trackArtist,
|
||||||
user,
|
user,
|
||||||
deviceConnection,
|
|
||||||
},
|
},
|
||||||
(r) => ({
|
(r) => ({
|
||||||
deviceConnection: {
|
|
||||||
user: r.one.user({
|
|
||||||
from: r.deviceConnection.userId,
|
|
||||||
to: r.user.id,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
artist: {
|
artist: {
|
||||||
artistGenres: r.many.artistGenre(),
|
artistGenres: r.many.artistGenre(),
|
||||||
artistImages: r.many.artistImage(),
|
artistImages: r.many.artistImage(),
|
||||||
|
|
@ -608,7 +601,6 @@ export const relations = defineRelations(
|
||||||
from: r.user.id,
|
from: r.user.id,
|
||||||
to: r.party.hostId,
|
to: r.party.hostId,
|
||||||
}),
|
}),
|
||||||
deviceConnection: r.many.deviceConnection(),
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -10,35 +10,14 @@ import { pubsub, topic } from "./party-socket";
|
||||||
|
|
||||||
type DeviceSocketMessage =
|
type DeviceSocketMessage =
|
||||||
| { type: "device_message"; deviceId: string; payload: unknown }
|
| { type: "device_message"; deviceId: string; payload: unknown }
|
||||||
| { type: "device_status_request"; deviceId: string }
|
|
||||||
| { type: "hello" }
|
| { type: "hello" }
|
||||||
| { type: "device_event"; deviceId: string; event: DeviceProxyEvent };
|
| { type: "device_event"; deviceId: string; event: PartySocketEvent };
|
||||||
|
|
||||||
type DeviceProxyEvent =
|
|
||||||
| PartySocketEvent
|
|
||||||
| { type: "device_connect_required" }
|
|
||||||
| { type: "device_connected" };
|
|
||||||
|
|
||||||
type DeviceQuizResponsePayload = {
|
type DeviceQuizResponsePayload = {
|
||||||
QuizResponse: number;
|
QuizResponse: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DeviceConnectionRecord = typeof deviceConnection.$inferSelect;
|
let devProxySocket: WebSocket | null = null;
|
||||||
|
|
||||||
type DevProxySocket = {
|
|
||||||
send: (message: string) => unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
let devProxySocket: DevProxySocket | null = null;
|
|
||||||
|
|
||||||
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string) {
|
|
||||||
return Promise.race([
|
|
||||||
promise,
|
|
||||||
new Promise<T>((_, reject) => {
|
|
||||||
setTimeout(() => reject(new Error(`${label} timed out`)), timeoutMs);
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isDeviceMessage(
|
function isDeviceMessage(
|
||||||
value: unknown,
|
value: unknown,
|
||||||
|
|
@ -51,17 +30,6 @@ function isDeviceMessage(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDeviceStatusRequestMessage(
|
|
||||||
value: unknown,
|
|
||||||
): value is Extract<DeviceSocketMessage, { type: "device_status_request" }> {
|
|
||||||
return (
|
|
||||||
typeof value === "object" &&
|
|
||||||
value !== null &&
|
|
||||||
(value as { type?: unknown }).type === "device_status_request" &&
|
|
||||||
typeof (value as { deviceId?: unknown }).deviceId === "string"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isDeviceQuizResponsePayload(
|
function isDeviceQuizResponsePayload(
|
||||||
value: unknown,
|
value: unknown,
|
||||||
): value is DeviceQuizResponsePayload {
|
): value is DeviceQuizResponsePayload {
|
||||||
|
|
@ -73,64 +41,16 @@ function isDeviceQuizResponsePayload(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendDeviceEvent(deviceId: string, event: DeviceProxyEvent) {
|
function sendDeviceEvent(deviceId: string, event: PartySocketEvent) {
|
||||||
if (!devProxySocket) {
|
if (!devProxySocket || devProxySocket.readyState !== WebSocket.OPEN) return;
|
||||||
console.log("[device-socket] no dev proxy for event", deviceId, event.type);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
devProxySocket.send(
|
||||||
console.log("[device-socket] sending event", deviceId, event.type);
|
JSON.stringify({
|
||||||
devProxySocket.send(
|
type: "device_event",
|
||||||
JSON.stringify({
|
|
||||||
type: "device_event",
|
|
||||||
deviceId,
|
|
||||||
event,
|
|
||||||
} satisfies DeviceSocketMessage),
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[device-socket] failed to send event", error);
|
|
||||||
devProxySocket = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function syncDeviceConnectionStatus(deviceId: string) {
|
|
||||||
console.log("[device-socket] status request", deviceId);
|
|
||||||
let device: DeviceConnectionRecord | undefined;
|
|
||||||
try {
|
|
||||||
console.log("[device-socket] lookup device start", deviceId);
|
|
||||||
device = await withTimeout(
|
|
||||||
db
|
|
||||||
.select()
|
|
||||||
.from(deviceConnection)
|
|
||||||
.where(eq(deviceConnection.id, deviceId))
|
|
||||||
.then((rows) => rows[0]),
|
|
||||||
2_000,
|
|
||||||
`device lookup ${deviceId}`,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
"[device-socket] lookup device result",
|
|
||||||
deviceId,
|
deviceId,
|
||||||
device ? "claimed" : "missing",
|
event,
|
||||||
);
|
} satisfies DeviceSocketMessage),
|
||||||
} catch (error) {
|
);
|
||||||
console.error("[device-socket] lookup device failed", deviceId, error);
|
|
||||||
sendDeviceEvent(deviceId, { type: "device_connect_required" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!device) {
|
|
||||||
console.log("[device-socket] device unclaimed", deviceId);
|
|
||||||
sendDeviceEvent(deviceId, { type: "device_connect_required" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("[device-socket] device claimed", deviceId, device.userId);
|
|
||||||
await db
|
|
||||||
.update(deviceConnection)
|
|
||||||
.set({ lastSeen: new Date() })
|
|
||||||
.where(eq(deviceConnection.id, deviceId));
|
|
||||||
sendDeviceEvent(deviceId, { type: "device_connected" });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function claimDeviceForUser(deviceId: string, userId: string) {
|
export async function claimDeviceForUser(deviceId: string, userId: string) {
|
||||||
|
|
@ -155,7 +75,7 @@ export async function publishDeviceEventForUser(
|
||||||
userId: string,
|
userId: string,
|
||||||
event: PartySocketEvent,
|
event: PartySocketEvent,
|
||||||
) {
|
) {
|
||||||
if (!devProxySocket) return;
|
if (!devProxySocket || devProxySocket.readyState !== WebSocket.OPEN) return;
|
||||||
|
|
||||||
const devices = await db
|
const devices = await db
|
||||||
.select()
|
.select()
|
||||||
|
|
@ -246,27 +166,25 @@ export const deviceSocketApp = new Elysia().group("/dev-socket", (app) =>
|
||||||
.get("/test", () => ({ ok: 1 }))
|
.get("/test", () => ({ ok: 1 }))
|
||||||
.ws("/ws", {
|
.ws("/ws", {
|
||||||
open(ws) {
|
open(ws) {
|
||||||
console.log("[device-socket] dev proxy connected");
|
devProxySocket = ws as unknown as WebSocket;
|
||||||
devProxySocket = ws;
|
|
||||||
ws.send(
|
ws.send(
|
||||||
JSON.stringify({ type: "hello" } satisfies DeviceSocketMessage),
|
JSON.stringify({ type: "hello" } satisfies DeviceSocketMessage),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
message: async (_ws, message: DeviceSocketMessage) => {
|
message: async (_ws, message) => {
|
||||||
if (typeof message !== "object") return;
|
if (typeof message !== "string") return;
|
||||||
console.log("[device-socket] received", message.type);
|
|
||||||
|
|
||||||
if (isDeviceStatusRequestMessage(message)) {
|
let parsed: DeviceSocketMessage;
|
||||||
await syncDeviceConnectionStatus(message.deviceId);
|
try {
|
||||||
|
parsed = JSON.parse(message) as DeviceSocketMessage;
|
||||||
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDeviceMessage(message)) {
|
if (!isDeviceMessage(parsed)) return;
|
||||||
await forwardDevicePayload(message.deviceId, message.payload);
|
await forwardDevicePayload(parsed.deviceId, parsed.payload);
|
||||||
}
|
|
||||||
},
|
},
|
||||||
close() {
|
close() {
|
||||||
console.log("[device-socket] dev proxy disconnected");
|
|
||||||
if (devProxySocket === null) return;
|
if (devProxySocket === null) return;
|
||||||
devProxySocket = null;
|
devProxySocket = null;
|
||||||
},
|
},
|
||||||
|
|
@ -280,7 +198,6 @@ export const deviceClaimApp = new Elysia()
|
||||||
"/:deviceId/connect",
|
"/:deviceId/connect",
|
||||||
async ({ user, params }) => {
|
async ({ user, params }) => {
|
||||||
await claimDeviceForUser(params.deviceId, user.id);
|
await claimDeviceForUser(params.deviceId, user.id);
|
||||||
sendDeviceEvent(params.deviceId, { type: "device_connected" });
|
|
||||||
return { ok: true, deviceId: params.deviceId, userId: user.id };
|
return { ok: true, deviceId: params.deviceId, userId: user.id };
|
||||||
},
|
},
|
||||||
{ auth: true },
|
{ auth: true },
|
||||||
|
|
|
||||||
|
|
@ -4,82 +4,58 @@ type ApiEnvelope =
|
||||||
| { type: "hello" }
|
| { type: "hello" }
|
||||||
| { type: "device_event"; deviceId: string; event: unknown };
|
| { type: "device_event"; deviceId: string; event: unknown };
|
||||||
|
|
||||||
type DeviceMessage =
|
type DeviceMessage = {
|
||||||
| {
|
DeviceId: string;
|
||||||
DeviceId: string;
|
} | {
|
||||||
}
|
QuizResponse: number;
|
||||||
| {
|
}
|
||||||
QuizResponse: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type DeviceQuestionData = {
|
type DeviceQuestionData = {
|
||||||
text: string;
|
text: string;
|
||||||
points: number;
|
points: number;
|
||||||
index: number;
|
index: number;
|
||||||
q_type: "Choice" | { Numeric: { min: number; max: number } };
|
q_type: "Choice" | { Numeric: { min: number; max: number } }
|
||||||
};
|
}
|
||||||
|
|
||||||
type ProxyOutput =
|
|
||||||
| { ConnectPrompt: string }
|
|
||||||
| "WaitingForParty"
|
|
||||||
| { Question: DeviceQuestionData }
|
|
||||||
| "Results"
|
|
||||||
| { Error: string };
|
|
||||||
|
|
||||||
type ApiMessage =
|
|
||||||
| { type: "device_status_request"; deviceId: string }
|
|
||||||
| { type: "device_message"; deviceId: string; payload: unknown };
|
|
||||||
|
|
||||||
type QuizQuestion =
|
type QuizQuestion =
|
||||||
| {
|
| {
|
||||||
type: "choice";
|
type: "choice";
|
||||||
text: string;
|
text: string;
|
||||||
points: number;
|
points: number;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "numeric";
|
type: "numeric";
|
||||||
text: string;
|
text: string;
|
||||||
points: number;
|
points: number;
|
||||||
range: { min: number; max: number };
|
range: { min: number; max: number };
|
||||||
};
|
};
|
||||||
|
|
||||||
type QuizState = {
|
type QuizState = {
|
||||||
status: "running" | "results";
|
status: "running" | "results";
|
||||||
questionIndex: number;
|
questionIndex: number;
|
||||||
currentQuestion: QuizQuestion | null;
|
currentQuestion: QuizQuestion | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PartyStatusEvent = {
|
type PartyStatusEvent = {
|
||||||
type: "party_status";
|
type: "party_status";
|
||||||
party: { data?: QuizState } | null;
|
party: { data?: QuizState } | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type QuizStateEvent = {
|
type QuizStateEvent = {
|
||||||
type: "quiz_state";
|
type: "quiz_state";
|
||||||
quiz: QuizState;
|
quiz: QuizState;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ErrorEvent = {
|
type ErrorEvent = {
|
||||||
type: "error";
|
type: "error";
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DeviceLifecycleEvent =
|
type PartySocketEvent = PartyStatusEvent | QuizStateEvent | ErrorEvent;
|
||||||
| { type: "device_connect_required" }
|
|
||||||
| { type: "device_connected" };
|
|
||||||
|
|
||||||
type PartySocketEvent =
|
|
||||||
| PartyStatusEvent
|
|
||||||
| QuizStateEvent
|
|
||||||
| ErrorEvent
|
|
||||||
| DeviceLifecycleEvent;
|
|
||||||
|
|
||||||
const sockets = new Map<string, Socket>();
|
const sockets = new Map<string, Socket>();
|
||||||
const socketIds = new WeakMap<Socket, string>();
|
const socketIds = new WeakMap<Socket, string>();
|
||||||
const API_SOCKET_URL = "ws://localhost:4000/api/dev-socket/ws";
|
const apiSocket = new WebSocket("ws://localhost:4000/api/dev-socket/ws");
|
||||||
let apiSocket: WebSocket | null = null;
|
|
||||||
let apiReconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
const pendingDeviceStatus = new Set<string>();
|
|
||||||
|
|
||||||
function socketDeviceId(socket: Socket) {
|
function socketDeviceId(socket: Socket) {
|
||||||
return socketIds.get(socket);
|
return socketIds.get(socket);
|
||||||
|
|
@ -89,155 +65,75 @@ function registerSocket(socket: Socket, deviceId: string) {
|
||||||
const existing = sockets.get(deviceId);
|
const existing = sockets.get(deviceId);
|
||||||
if (existing && existing !== socket) existing.end();
|
if (existing && existing !== socket) existing.end();
|
||||||
sockets.set(deviceId, socket);
|
sockets.set(deviceId, socket);
|
||||||
socketIds.set(socket, deviceId);
|
socketIds.set(socket, deviceId);
|
||||||
console.log("Registered", socket.remoteAddress, deviceId);
|
console.log("Registered", socket.remoteAddress, deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeProxyOutput(socket: Socket, output: ProxyOutput) {
|
function toDeviceQuestionData(
|
||||||
socket.write(`${JSON.stringify(output)}\n`);
|
quizData: QuizState,
|
||||||
}
|
): DeviceQuestionData | null {
|
||||||
|
if (!quizData.currentQuestion) return null;
|
||||||
|
const question = quizData.currentQuestion;
|
||||||
|
const q_type =
|
||||||
|
question.type === "choice"
|
||||||
|
? "Choice"
|
||||||
|
: { Numeric: { min: question.range.min, max: question.range.max } };
|
||||||
|
|
||||||
function sendApiMessage(message: ApiMessage) {
|
return {
|
||||||
if (!apiSocket || apiSocket.readyState !== WebSocket.OPEN) return false;
|
text: question.text,
|
||||||
console.log("API send", message.type, message.deviceId);
|
points: question.points,
|
||||||
apiSocket.send(JSON.stringify(message));
|
index: quizData.questionIndex,
|
||||||
return true;
|
q_type,
|
||||||
}
|
};
|
||||||
|
|
||||||
function requestDeviceStatus(deviceId: string) {
|
|
||||||
pendingDeviceStatus.add(deviceId);
|
|
||||||
if (sendApiMessage({ type: "device_status_request", deviceId })) {
|
|
||||||
console.log("Requested device status", deviceId);
|
|
||||||
pendingDeviceStatus.delete(deviceId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log("Queued device status request", deviceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function flushPendingDeviceStatus() {
|
|
||||||
for (const deviceId of pendingDeviceStatus) {
|
|
||||||
if (
|
|
||||||
sockets.has(deviceId) &&
|
|
||||||
sendApiMessage({ type: "device_status_request", deviceId })
|
|
||||||
) {
|
|
||||||
console.log("Flushed device status request", deviceId);
|
|
||||||
pendingDeviceStatus.delete(deviceId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function disconnectDeviceClients(reason: string) {
|
|
||||||
console.log("Disconnecting device clients", reason, sockets.size);
|
|
||||||
for (const socket of sockets.values()) {
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
sockets.clear();
|
|
||||||
pendingDeviceStatus.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
function scheduleApiReconnect() {
|
|
||||||
if (apiReconnectTimer) return;
|
|
||||||
apiReconnectTimer = setTimeout(() => {
|
|
||||||
apiReconnectTimer = null;
|
|
||||||
connectApiSocket();
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
function connectApiSocket() {
|
|
||||||
if (
|
|
||||||
apiSocket?.readyState === WebSocket.OPEN ||
|
|
||||||
apiSocket?.readyState === WebSocket.CONNECTING
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Connecting to API device socket");
|
|
||||||
apiSocket = new WebSocket(API_SOCKET_URL);
|
|
||||||
apiSocket.onmessage = handleApiMessage;
|
|
||||||
apiSocket.onerror = (error) => {
|
|
||||||
console.error("API device socket error", error);
|
|
||||||
};
|
|
||||||
apiSocket.onclose = () => {
|
|
||||||
console.log("API device socket closed; reconnecting");
|
|
||||||
apiSocket = null;
|
|
||||||
disconnectDeviceClients("api socket closed");
|
|
||||||
scheduleApiReconnect();
|
|
||||||
};
|
|
||||||
apiSocket.onopen = () => {
|
|
||||||
console.log("Connected to API device socket");
|
|
||||||
flushPendingDeviceStatus();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function toDeviceQuestionData(quizData: QuizState): DeviceQuestionData | null {
|
|
||||||
if (!quizData.currentQuestion) return null;
|
|
||||||
const question = quizData.currentQuestion;
|
|
||||||
const q_type =
|
|
||||||
question.type === "choice"
|
|
||||||
? "Choice"
|
|
||||||
: { Numeric: { min: question.range.min, max: question.range.max } };
|
|
||||||
|
|
||||||
return {
|
|
||||||
text: question.text,
|
|
||||||
points: question.points,
|
|
||||||
index: quizData.questionIndex,
|
|
||||||
q_type,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const listener = Bun.listen({
|
const listener = Bun.listen({
|
||||||
port: 7070,
|
port: 7070,
|
||||||
hostname: "0.0.0.0",
|
hostname: "0.0.0.0",
|
||||||
socket: {
|
socket: {
|
||||||
open(socket) {
|
open(socket) {
|
||||||
socket.setKeepAlive(true);
|
socket.setKeepAlive(true);
|
||||||
console.log("Connection", socket.remoteAddress, socket.remotePort);
|
console.log("Connection", socket.remoteAddress, socket.remotePort);
|
||||||
},
|
},
|
||||||
data(socket, buf) {
|
data(socket, buf) {
|
||||||
const raw = new TextDecoder().decode(buf).trim();
|
const raw = new TextDecoder().decode(buf).trim();
|
||||||
let data: DeviceMessage;
|
let data: DeviceMessage;
|
||||||
try {
|
try {
|
||||||
data = JSON.parse(raw);
|
data = JSON.parse(raw);
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log("Data", socket.remoteAddress, data);
|
console.log("Data", socket.remoteAddress, data);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
if ("DeviceId" in data) {
|
if ("DeviceId" in data) {
|
||||||
registerSocket(socket, data.DeviceId);
|
registerSocket(socket, data.DeviceId);
|
||||||
console.log(
|
return;
|
||||||
"Requesting device status",
|
}
|
||||||
data.DeviceId,
|
if ("QuizResponse" in data) {
|
||||||
"apiState",
|
const deviceId = socketDeviceId(socket);
|
||||||
apiSocket?.readyState,
|
if (!deviceId) return;
|
||||||
);
|
apiSocket?.send(
|
||||||
requestDeviceStatus(data.DeviceId);
|
JSON.stringify({
|
||||||
return;
|
type: "device_message",
|
||||||
}
|
deviceId,
|
||||||
if ("QuizResponse" in data) {
|
payload: { QuizResponse: data.QuizResponse },
|
||||||
const deviceId = socketDeviceId(socket);
|
}),
|
||||||
if (!deviceId) return;
|
);
|
||||||
sendApiMessage({
|
return;
|
||||||
type: "device_message",
|
}
|
||||||
deviceId,
|
|
||||||
payload: { QuizResponse: data.QuizResponse },
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
close(socket) {
|
close(socket) {
|
||||||
console.log("Connection", socket.remoteAddress);
|
console.log("Connection", socket.remoteAddress);
|
||||||
const deviceId = socketDeviceId(socket);
|
const deviceId = socketDeviceId(socket);
|
||||||
if (deviceId && sockets.get(deviceId) === socket) {
|
if (deviceId && sockets.get(deviceId) === socket) {
|
||||||
sockets.delete(deviceId);
|
sockets.delete(deviceId);
|
||||||
pendingDeviceStatus.delete(deviceId);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleApiMessage(e: MessageEvent) {
|
apiSocket.onmessage = (e) => {
|
||||||
let message: ApiEnvelope;
|
let message: ApiEnvelope;
|
||||||
try {
|
try {
|
||||||
message = JSON.parse(e.data) as ApiEnvelope;
|
message = JSON.parse(e.data) as ApiEnvelope;
|
||||||
|
|
@ -245,29 +141,12 @@ function handleApiMessage(e: MessageEvent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("API recv", message.type);
|
|
||||||
if (message.type !== "device_event") return;
|
if (message.type !== "device_event") return;
|
||||||
const socket = sockets.get(message.deviceId);
|
const socket = sockets.get(message.deviceId);
|
||||||
if (!socket) {
|
if (!socket) return;
|
||||||
console.log("No TCP socket for API event", message.deviceId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const event = message.event as PartySocketEvent;
|
const event = message.event as PartySocketEvent;
|
||||||
console.log("API device event", message.deviceId, event.type);
|
|
||||||
if (event.type === "device_connect_required") {
|
|
||||||
console.log("Writing connect prompt", message.deviceId);
|
|
||||||
writeProxyOutput(socket, { ConnectPrompt: message.deviceId });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.type === "device_connected") {
|
|
||||||
console.log("Writing waiting-for-party", message.deviceId);
|
|
||||||
writeProxyOutput(socket, "WaitingForParty");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.type === "error") {
|
if (event.type === "error") {
|
||||||
writeProxyOutput(socket, { Error: event.message });
|
socket.write(`${JSON.stringify({ Error: event.message })}\n`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -275,27 +154,31 @@ function handleApiMessage(e: MessageEvent) {
|
||||||
const quizData = event.party?.data ?? null;
|
const quizData = event.party?.data ?? null;
|
||||||
if (!quizData) return;
|
if (!quizData) return;
|
||||||
const question = toDeviceQuestionData(quizData);
|
const question = toDeviceQuestionData(quizData);
|
||||||
if (question) {
|
socket.write(
|
||||||
writeProxyOutput(socket, { Question: question });
|
`${JSON.stringify({ Question: question, Status: quizData.status })}\n`,
|
||||||
} else if (quizData.status === "results") {
|
);
|
||||||
writeProxyOutput(socket, "Results");
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type === "quiz_state") {
|
if (event.type === "quiz_state") {
|
||||||
const question = toDeviceQuestionData(event.quiz);
|
const question = toDeviceQuestionData(event.quiz);
|
||||||
if (question) {
|
socket.write(
|
||||||
writeProxyOutput(socket, { Question: question });
|
`${JSON.stringify({ Question: question, Status: event.quiz.status })}\n`,
|
||||||
} else if (event.quiz.status === "results") {
|
);
|
||||||
writeProxyOutput(socket, "Results");
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
writeProxyOutput(socket, { Error: "Unsupported proxy event." });
|
socket.write(`${JSON.stringify(message.event)}\n`);
|
||||||
}
|
};
|
||||||
|
|
||||||
connectApiSocket();
|
|
||||||
|
|
||||||
|
apiSocket.onerror = (error) => {
|
||||||
|
console.error(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
apiSocket.onopen = () => {
|
||||||
|
console.log("Connected to API device socket");
|
||||||
|
};
|
||||||
|
|
||||||
console.log(`Started on :${listener.port}`);
|
console.log(`Started on :${listener.port}`);
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "dev-proxy",
|
"name": "dev-proxy",
|
||||||
"module": "index.ts",
|
"module": "index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest"
|
"@types/bun": "latest"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,29 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// Environment setup & latest features
|
// Environment setup & latest features
|
||||||
"lib": ["ESNext"],
|
"lib": ["ESNext"],
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"module": "Preserve",
|
"module": "Preserve",
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
|
|
||||||
// Bundler mode
|
// Bundler mode
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
|
||||||
// Best practices
|
// Best practices
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedIndexedAccess": true,
|
"noUncheckedIndexedAccess": true,
|
||||||
"noImplicitOverride": true,
|
"noImplicitOverride": true,
|
||||||
|
|
||||||
// Some stricter flags (disabled by default)
|
// Some stricter flags (disabled by default)
|
||||||
"noUnusedLocals": false,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": false,
|
"noUnusedParameters": false,
|
||||||
"noPropertyAccessFromIndexSignature": false
|
"noPropertyAccessFromIndexSignature": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,6 @@ const DOT: char = char::from_u32(0b1010_0101).unwrap();
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum ViewState {
|
pub enum ViewState {
|
||||||
Loading,
|
Loading,
|
||||||
Reconnecting,
|
|
||||||
ConnectPrompt,
|
|
||||||
WaitingForParty,
|
|
||||||
Question,
|
Question,
|
||||||
Results,
|
Results,
|
||||||
}
|
}
|
||||||
|
|
@ -47,8 +44,6 @@ pub struct QuestionDataNet<'a> {
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub enum ProxyOutput<'a> {
|
pub enum ProxyOutput<'a> {
|
||||||
ConnectPrompt(&'a str),
|
|
||||||
WaitingForParty,
|
|
||||||
Question(QuestionDataNet<'a>),
|
Question(QuestionDataNet<'a>),
|
||||||
Results,
|
Results,
|
||||||
Error(&'a str),
|
Error(&'a str),
|
||||||
|
|
@ -86,7 +81,6 @@ impl WheelData {
|
||||||
|
|
||||||
pub struct DeviceState {
|
pub struct DeviceState {
|
||||||
view: ViewState,
|
view: ViewState,
|
||||||
device_id: Option<OwnedStr<64>>,
|
|
||||||
question: Option<QuestionData>,
|
question: Option<QuestionData>,
|
||||||
wheel: WheelData,
|
wheel: WheelData,
|
||||||
last_index: usize,
|
last_index: usize,
|
||||||
|
|
@ -97,7 +91,6 @@ impl DeviceState {
|
||||||
pub const fn new() -> Self {
|
pub const fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
view: ViewState::Loading,
|
view: ViewState::Loading,
|
||||||
device_id: None,
|
|
||||||
question: None,
|
question: None,
|
||||||
wheel: WheelData::empty(),
|
wheel: WheelData::empty(),
|
||||||
last_index: 0,
|
last_index: 0,
|
||||||
|
|
@ -106,19 +99,12 @@ impl DeviceState {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(&mut self) {
|
pub fn reset(&mut self) {
|
||||||
self.view = ViewState::Loading;
|
|
||||||
self.question = None;
|
self.question = None;
|
||||||
self.wheel = WheelData::empty();
|
self.wheel = WheelData::empty();
|
||||||
self.last_index = 0;
|
self.last_index = 0;
|
||||||
self.title_offset = 0;
|
self.title_offset = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reconnecting(&mut self) {
|
|
||||||
self.question = None;
|
|
||||||
self.wheel = WheelData::empty();
|
|
||||||
self.view = ViewState::Reconnecting;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn view_state(&self) -> ViewState {
|
pub fn view_state(&self) -> ViewState {
|
||||||
self.view
|
self.view
|
||||||
}
|
}
|
||||||
|
|
@ -133,21 +119,6 @@ impl DeviceState {
|
||||||
|
|
||||||
pub fn apply_proxy_output(&mut self, data: ProxyOutput<'_>) {
|
pub fn apply_proxy_output(&mut self, data: ProxyOutput<'_>) {
|
||||||
match data {
|
match data {
|
||||||
ProxyOutput::ConnectPrompt(device_id) => {
|
|
||||||
let mut owned_device_id = OwnedStr::new();
|
|
||||||
for char in device_id.chars() {
|
|
||||||
if owned_device_id.try_push(char).is_err() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.device_id = Some(owned_device_id);
|
|
||||||
self.question = None;
|
|
||||||
self.view = ViewState::ConnectPrompt;
|
|
||||||
}
|
|
||||||
ProxyOutput::WaitingForParty => {
|
|
||||||
self.question = None;
|
|
||||||
self.view = ViewState::WaitingForParty;
|
|
||||||
}
|
|
||||||
ProxyOutput::Question(data) => {
|
ProxyOutput::Question(data) => {
|
||||||
let data: QuestionData = data.into();
|
let data: QuestionData = data.into();
|
||||||
let mut future_wheel = WheelData::empty();
|
let mut future_wheel = WheelData::empty();
|
||||||
|
|
@ -186,41 +157,6 @@ impl DeviceState {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_lines(&mut self) -> Option<(OwnedStr<16>, OwnedStr<16>)> {
|
pub fn render_lines(&mut self) -> Option<(OwnedStr<16>, OwnedStr<16>)> {
|
||||||
if self.view == ViewState::Loading {
|
|
||||||
return Some((
|
|
||||||
OwnedStr::from_str("Connecting").unwrap(),
|
|
||||||
OwnedStr::from_str("Please wait").unwrap(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.view == ViewState::Reconnecting {
|
|
||||||
return Some((
|
|
||||||
OwnedStr::from_str("Reconnecting").unwrap(),
|
|
||||||
OwnedStr::from_str("Please wait").unwrap(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.view == ViewState::ConnectPrompt {
|
|
||||||
let device_id = self.device_id.as_ref()?;
|
|
||||||
let mut display_id: OwnedStr<16> = OwnedStr::new();
|
|
||||||
for char in device_id.as_str().chars() {
|
|
||||||
if display_id.try_push(char).is_err() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Some((
|
|
||||||
OwnedStr::from_str("Connect device").unwrap(),
|
|
||||||
center_str::<16>(display_id.as_str(), 16).unwrap(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.view == ViewState::WaitingForParty {
|
|
||||||
return Some((
|
|
||||||
OwnedStr::from_str("Connected").unwrap(),
|
|
||||||
OwnedStr::from_str("Waiting party").unwrap(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.view != ViewState::Question {
|
if self.view != ViewState::Question {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
@ -297,7 +233,11 @@ pub fn wheel_delta(old_angle: i32, current_angle: i32, inverted: bool) -> i32 {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if inverted { -diff } else { diff }
|
if inverted {
|
||||||
|
-diff
|
||||||
|
} else {
|
||||||
|
diff
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply_wheel_delta(
|
pub fn apply_wheel_delta(
|
||||||
|
|
@ -364,10 +304,7 @@ impl<const CAP: usize> ufmt::uWrite for OwnedStrWriter<CAP> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn center_str<const CAP: usize>(
|
pub fn center_str<const CAP: usize>(text: &str, width: usize) -> Result<OwnedStr<CAP>, owned_str::Error> {
|
||||||
text: &str,
|
|
||||||
width: usize,
|
|
||||||
) -> Result<OwnedStr<CAP>, owned_str::Error> {
|
|
||||||
let mut res = OwnedStr::new();
|
let mut res = OwnedStr::new();
|
||||||
let len = text.len();
|
let len = text.len();
|
||||||
let padding = (width.saturating_sub(len) + 1) / 2;
|
let padding = (width.saturating_sub(len) + 1) / 2;
|
||||||
|
|
@ -380,65 +317,7 @@ pub fn center_str<const CAP: usize>(
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{DeviceState, ViewState, apply_wheel_delta, parse_proxy_output, wheel_delta};
|
use super::{apply_wheel_delta, wheel_delta};
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parses_and_renders_connect_prompt() {
|
|
||||||
let data = parse_proxy_output(r#"{"ConnectPrompt":"esp32-1"}"#).unwrap();
|
|
||||||
let mut state = DeviceState::new();
|
|
||||||
|
|
||||||
state.apply_proxy_output(data);
|
|
||||||
|
|
||||||
assert_eq!(state.view_state(), ViewState::ConnectPrompt);
|
|
||||||
let (line1, line2) = state.render_lines().unwrap();
|
|
||||||
assert_eq!(line1.as_str(), "Connect device");
|
|
||||||
assert_eq!(line2.as_str(), " esp32-1");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parses_results_message() {
|
|
||||||
let data = parse_proxy_output(r#""Results""#).unwrap();
|
|
||||||
let mut state = DeviceState::new();
|
|
||||||
|
|
||||||
state.apply_proxy_output(data);
|
|
||||||
|
|
||||||
assert_eq!(state.view_state(), ViewState::Results);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parses_and_renders_waiting_for_party() {
|
|
||||||
let data = parse_proxy_output(r#""WaitingForParty""#).unwrap();
|
|
||||||
let mut state = DeviceState::new();
|
|
||||||
|
|
||||||
state.apply_proxy_output(data);
|
|
||||||
|
|
||||||
assert_eq!(state.view_state(), ViewState::WaitingForParty);
|
|
||||||
let (line1, line2) = state.render_lines().unwrap();
|
|
||||||
assert_eq!(line1.as_str(), "Connected");
|
|
||||||
assert_eq!(line2.as_str(), "Waiting party");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn renders_reconnecting_state() {
|
|
||||||
let mut state = DeviceState::new();
|
|
||||||
|
|
||||||
state.reconnecting();
|
|
||||||
|
|
||||||
assert_eq!(state.view_state(), ViewState::Reconnecting);
|
|
||||||
let (line1, line2) = state.render_lines().unwrap();
|
|
||||||
assert_eq!(line1.as_str(), "Reconnecting");
|
|
||||||
assert_eq!(line2.as_str(), "Please wait");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn renders_loading_state() {
|
|
||||||
let mut state = DeviceState::new();
|
|
||||||
|
|
||||||
let (line1, line2) = state.render_lines().unwrap();
|
|
||||||
|
|
||||||
assert_eq!(line1.as_str(), "Connecting");
|
|
||||||
assert_eq!(line2.as_str(), "Please wait");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn wraps_forward_across_zero() {
|
fn wraps_forward_across_zero() {
|
||||||
|
|
|
||||||
|
|
@ -49,11 +49,6 @@ pub async fn reset_state() {
|
||||||
state.reset();
|
state.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn reconnecting_state() {
|
|
||||||
let mut state = STATE.lock().await;
|
|
||||||
state.reconnecting();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[panic_handler]
|
#[panic_handler]
|
||||||
fn panic(info: &core::panic::PanicInfo) -> ! {
|
fn panic(info: &core::panic::PanicInfo) -> ! {
|
||||||
println!("PANIC! {:?}", info);
|
println!("PANIC! {:?}", info);
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,8 @@ use esp_radio::wifi::sta::StationConfig;
|
||||||
use esp_radio::wifi::{Config, ControllerConfig, scan::ScanConfig};
|
use esp_radio::wifi::{Config, ControllerConfig, scan::ScanConfig};
|
||||||
|
|
||||||
use crate::screen::overwrite_lcd;
|
use crate::screen::overwrite_lcd;
|
||||||
use crate::{WIFI_NETWORK, WIFI_PASSWORD};
|
|
||||||
use crate::{buffer::wait_for_config, tcp_read_loop, tcp_write_loop};
|
use crate::{buffer::wait_for_config, tcp_read_loop, tcp_write_loop};
|
||||||
use crate::{reconnecting_state, reset_state};
|
use crate::{WIFI_NETWORK, WIFI_PASSWORD};
|
||||||
|
|
||||||
pub struct NetworkConfig<'a> {
|
pub struct NetworkConfig<'a> {
|
||||||
pub wifi: WIFI<'a>,
|
pub wifi: WIFI<'a>,
|
||||||
|
|
@ -130,12 +129,10 @@ pub async fn network_setup_task(
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
println!("tcp connect error: {:?}", e);
|
println!("tcp connect error: {:?}", e);
|
||||||
reconnecting_state().await;
|
|
||||||
overwrite_lcd("TCP error", &format!("{}", e)).await;
|
overwrite_lcd("TCP error", &format!("{}", e)).await;
|
||||||
Timer::after_millis(1000).await;
|
Timer::after_millis(1000).await;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
reset_state().await;
|
|
||||||
overwrite_lcd("Connected", "").await;
|
overwrite_lcd("Connected", "").await;
|
||||||
|
|
||||||
let cancel = Signal::<CriticalSectionRawMutex, ()>::new();
|
let cancel = Signal::<CriticalSectionRawMutex, ()>::new();
|
||||||
|
|
@ -152,11 +149,10 @@ pub async fn network_setup_task(
|
||||||
|
|
||||||
if !stack.is_config_up() {
|
if !stack.is_config_up() {
|
||||||
println!("wifi down, reconnecting wifi");
|
println!("wifi down, reconnecting wifi");
|
||||||
reconnecting_state().await;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
reconnecting_state().await;
|
overwrite_lcd("Connection close", "").await;
|
||||||
Timer::after_millis(500).await;
|
Timer::after_millis(500).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ use std::sync::{Arc, Mutex};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use crossterm::cursor::Show;
|
|
||||||
use crossterm::event;
|
|
||||||
use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers};
|
use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers};
|
||||||
use crossterm::queue;
|
|
||||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||||
|
use crossterm::cursor::Show;
|
||||||
|
use crossterm::queue;
|
||||||
|
use crossterm::event;
|
||||||
use device_state::{DeviceState, WriteType, apply_wheel_delta};
|
use device_state::{DeviceState, WriteType, apply_wheel_delta};
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
|
|
@ -88,7 +88,6 @@ async fn main() -> io::Result<()> {
|
||||||
Ok(stream) => break stream,
|
Ok(stream) => break stream,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log_error(&format!("connect error: {err}"));
|
log_error(&format!("connect error: {err}"));
|
||||||
state.lock().unwrap().reconnecting();
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = &mut sigint => {
|
_ = &mut sigint => {
|
||||||
log_error("received SIGINT");
|
log_error("received SIGINT");
|
||||||
|
|
@ -108,12 +107,10 @@ async fn main() -> io::Result<()> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let (mut read, mut write) = stream.into_split();
|
let (mut read, mut write) = stream.into_split();
|
||||||
state.lock().unwrap().reset();
|
|
||||||
|
|
||||||
let device_id = device_state::serialize_write(&WriteType::DeviceId(DEVICE_ID)).unwrap();
|
let device_id = device_state::serialize_write(&WriteType::DeviceId(DEVICE_ID)).unwrap();
|
||||||
if write.write_all(device_id.as_bytes()).await.is_err() {
|
if write.write_all(device_id.as_bytes()).await.is_err() {
|
||||||
log_error("failed to send device id");
|
log_error("failed to send device id");
|
||||||
state.lock().unwrap().reconnecting();
|
|
||||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -187,7 +184,6 @@ async fn main() -> io::Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
log_error("reconnecting in 500ms");
|
log_error("reconnecting in 500ms");
|
||||||
state.lock().unwrap().reconnecting();
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = &mut sigint => {
|
_ = &mut sigint => {
|
||||||
log_error("received SIGINT");
|
log_error("received SIGINT");
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,7 @@ import {
|
||||||
|
|
||||||
export function UserInfo() {
|
export function UserInfo() {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { party, members, isConnecting, isReconnecting, resetParty } =
|
const { party, members, isConnecting, isReconnecting } = useParty();
|
||||||
useParty();
|
|
||||||
return (
|
return (
|
||||||
<Item>
|
<Item>
|
||||||
<ItemMedia>
|
<ItemMedia>
|
||||||
|
|
@ -38,15 +37,8 @@ export function UserInfo() {
|
||||||
</ItemDescription>
|
</ItemDescription>
|
||||||
</ItemContent>
|
</ItemContent>
|
||||||
<ItemActions>
|
<ItemActions>
|
||||||
{party && (
|
{party && members.length > 1 && (
|
||||||
<Button
|
<Button onClick={() => client.api.party.leave.post()}>Leave</Button>
|
||||||
onClick={async () => {
|
|
||||||
await client.api.party.leave.post();
|
|
||||||
resetParty();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Leave
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
</ItemActions>
|
</ItemActions>
|
||||||
</Item>
|
</Item>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,4 @@
|
||||||
import {
|
import { useCallback, useMemo, useState } from "react";
|
||||||
createContext,
|
|
||||||
createElement,
|
|
||||||
type ReactNode,
|
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import type {
|
import type {
|
||||||
PartySocketEvent,
|
PartySocketEvent,
|
||||||
PartyState,
|
PartyState,
|
||||||
|
|
@ -15,34 +6,14 @@ import type {
|
||||||
import { usePartySocket } from "./use-party-socket";
|
import { usePartySocket } from "./use-party-socket";
|
||||||
import { useUser } from "./user";
|
import { useUser } from "./user";
|
||||||
|
|
||||||
const emptyPartyState: PartyState = {
|
|
||||||
party: null,
|
|
||||||
members: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
type PartyContextValue = PartyState & {
|
|
||||||
connectionState: "disconnected" | "connecting" | "connected" | "reconnecting";
|
|
||||||
isConnected: boolean;
|
|
||||||
isConnecting: boolean;
|
|
||||||
isReconnecting: boolean;
|
|
||||||
setPartyState: (state: PartyState) => void;
|
|
||||||
resetParty: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const PartyContext = createContext<PartyContextValue | null>(null);
|
|
||||||
|
|
||||||
function reducePartyState(
|
function reducePartyState(
|
||||||
state: PartyState,
|
state: PartyState,
|
||||||
event: PartySocketEvent,
|
event: PartySocketEvent,
|
||||||
userId: string,
|
|
||||||
): PartyState {
|
): PartyState {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case "party_status": {
|
case "snapshot":
|
||||||
if (!event.party) return emptyPartyState;
|
case "party_status":
|
||||||
const isMember = event.members.some((member) => member.userId === userId);
|
|
||||||
if (!isMember) return emptyPartyState;
|
|
||||||
return { party: event.party, members: event.members };
|
return { party: event.party, members: event.members };
|
||||||
}
|
|
||||||
case "member_payload":
|
case "member_payload":
|
||||||
case "pong":
|
case "pong":
|
||||||
case "error":
|
case "error":
|
||||||
|
|
@ -57,18 +28,16 @@ function getApiUrl(): string | null {
|
||||||
return `${window.location.protocol}//${window.location.host}`;
|
return `${window.location.protocol}//${window.location.host}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PartyProvider({ children }: { children: ReactNode }) {
|
export function useParty() {
|
||||||
const { user } = useUser();
|
const { session } = useUser();
|
||||||
const userId = user?.id ?? null;
|
const [state, setState] = useState<PartyState>({
|
||||||
const [state, setState] = useState<PartyState>(emptyPartyState);
|
party: null,
|
||||||
|
members: [],
|
||||||
|
});
|
||||||
|
|
||||||
const handleMessage = useCallback(
|
const handleMessage = useCallback((event: PartySocketEvent) => {
|
||||||
(event: PartySocketEvent) => {
|
setState((prev: PartyState) => reducePartyState(prev, event));
|
||||||
if (!userId) return;
|
}, []);
|
||||||
setState((prev: PartyState) => reducePartyState(prev, event, userId));
|
|
||||||
},
|
|
||||||
[userId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const apiUrl = useMemo(() => {
|
const apiUrl = useMemo(() => {
|
||||||
const url = getApiUrl();
|
const url = getApiUrl();
|
||||||
|
|
@ -76,36 +45,13 @@ export function PartyProvider({ children }: { children: ReactNode }) {
|
||||||
return url;
|
return url;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const resetParty = useCallback(() => {
|
|
||||||
setState(emptyPartyState);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!userId) resetParty();
|
|
||||||
}, [resetParty, userId]);
|
|
||||||
|
|
||||||
const wsState = usePartySocket({
|
const wsState = usePartySocket({
|
||||||
apiUrl: userId ? apiUrl : null,
|
apiUrl,
|
||||||
onMessage: userId ? handleMessage : null,
|
onMessage: session ? handleMessage : null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const value = useMemo(
|
return {
|
||||||
() => ({
|
...state,
|
||||||
...state,
|
...wsState,
|
||||||
...wsState,
|
};
|
||||||
setPartyState: setState,
|
|
||||||
resetParty,
|
|
||||||
}),
|
|
||||||
[state, wsState, resetParty],
|
|
||||||
);
|
|
||||||
|
|
||||||
return createElement(PartyContext.Provider, { value }, children);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useParty() {
|
|
||||||
const context = useContext(PartyContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useParty must be used within PartyProvider");
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
import { treaty } from "@elysiajs/eden";
|
import { treaty } from "@elysiajs/eden";
|
||||||
import type { App } from "../../../api/src/index";
|
import type { App } from "../../../api/src/index";
|
||||||
|
|
||||||
// export const client = treaty<App>("aura.rpi1.danbulant.cloud", {});
|
export const client = treaty<App>("aura.rpi1.danbulant.cloud", {});
|
||||||
export const client = treaty<App>(
|
|
||||||
process.env.VITE_BETTER_AUTH_URL || "127.0.0.1:3000",
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
|
||||||
import type * as React from "react";
|
import type * as React from "react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { PartyProvider } from "#/hooks/use-party";
|
|
||||||
import type { AuthSession } from "#/lib/auth.serverfn";
|
import type { AuthSession } from "#/lib/auth.serverfn";
|
||||||
import { fetchSession, sessionQueryKey } from "#/lib/auth-client";
|
import { fetchSession, sessionQueryKey } from "#/lib/auth-client";
|
||||||
import { client } from "#/lib/eden";
|
import { client } from "#/lib/eden";
|
||||||
|
|
@ -132,7 +131,7 @@ function RootDocument({ children }: { children: React.ReactNode }) {
|
||||||
<HeadContent />
|
<HeadContent />
|
||||||
</head>
|
</head>
|
||||||
<body className="font-sans antialiased wrap-anywhere dark">
|
<body className="font-sans antialiased wrap-anywhere dark">
|
||||||
<PartyProvider>{children}</PartyProvider>
|
{children}
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<TanStackDevtools
|
<TanStackDevtools
|
||||||
config={{
|
config={{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue