waiting for party state
This commit is contained in:
parent
0daba1bd73
commit
cf66d9af6d
3 changed files with 115 additions and 13 deletions
|
|
@ -10,8 +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_connected"; deviceId: string }
|
||||||
| { type: "hello" }
|
| { type: "hello" }
|
||||||
| { type: "device_event"; deviceId: string; event: PartySocketEvent };
|
| { type: "device_event"; deviceId: string; event: DeviceProxyEvent };
|
||||||
|
|
||||||
|
type DeviceProxyEvent =
|
||||||
|
| PartySocketEvent
|
||||||
|
| { type: "device_connect_required" }
|
||||||
|
| { type: "device_connected" };
|
||||||
|
|
||||||
type DeviceQuizResponsePayload = {
|
type DeviceQuizResponsePayload = {
|
||||||
QuizResponse: number;
|
QuizResponse: number;
|
||||||
|
|
@ -30,6 +36,17 @@ function isDeviceMessage(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isDeviceConnectedMessage(
|
||||||
|
value: unknown,
|
||||||
|
): value is Extract<DeviceSocketMessage, { type: "device_connected" }> {
|
||||||
|
return (
|
||||||
|
typeof value === "object" &&
|
||||||
|
value !== null &&
|
||||||
|
(value as { type?: unknown }).type === "device_connected" &&
|
||||||
|
typeof (value as { deviceId?: unknown }).deviceId === "string"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function isDeviceQuizResponsePayload(
|
function isDeviceQuizResponsePayload(
|
||||||
value: unknown,
|
value: unknown,
|
||||||
): value is DeviceQuizResponsePayload {
|
): value is DeviceQuizResponsePayload {
|
||||||
|
|
@ -41,7 +58,7 @@ function isDeviceQuizResponsePayload(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendDeviceEvent(deviceId: string, event: PartySocketEvent) {
|
function sendDeviceEvent(deviceId: string, event: DeviceProxyEvent) {
|
||||||
if (!devProxySocket || devProxySocket.readyState !== WebSocket.OPEN) return;
|
if (!devProxySocket || devProxySocket.readyState !== WebSocket.OPEN) return;
|
||||||
|
|
||||||
devProxySocket.send(
|
devProxySocket.send(
|
||||||
|
|
@ -53,6 +70,25 @@ function sendDeviceEvent(deviceId: string, event: PartySocketEvent) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function syncDeviceConnectionStatus(deviceId: string) {
|
||||||
|
const device = await db
|
||||||
|
.select()
|
||||||
|
.from(deviceConnection)
|
||||||
|
.where(eq(deviceConnection.id, deviceId))
|
||||||
|
.then((rows) => rows[0]);
|
||||||
|
|
||||||
|
if (!device) {
|
||||||
|
sendDeviceEvent(deviceId, { type: "device_connect_required" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
await db
|
await db
|
||||||
.insert(deviceConnection)
|
.insert(deviceConnection)
|
||||||
|
|
@ -181,8 +217,14 @@ export const deviceSocketApp = new Elysia().group("/dev-socket", (app) =>
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isDeviceMessage(parsed)) return;
|
if (isDeviceConnectedMessage(parsed)) {
|
||||||
await forwardDevicePayload(parsed.deviceId, parsed.payload);
|
await syncDeviceConnectionStatus(parsed.deviceId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDeviceMessage(parsed)) {
|
||||||
|
await forwardDevicePayload(parsed.deviceId, parsed.payload);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
close() {
|
close() {
|
||||||
if (devProxySocket === null) return;
|
if (devProxySocket === null) return;
|
||||||
|
|
@ -198,6 +240,7 @@ 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 },
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,15 @@ type DeviceQuestionData = {
|
||||||
|
|
||||||
type ProxyOutput =
|
type ProxyOutput =
|
||||||
| { ConnectPrompt: string }
|
| { ConnectPrompt: string }
|
||||||
|
| "WaitingForParty"
|
||||||
| { Question: DeviceQuestionData }
|
| { Question: DeviceQuestionData }
|
||||||
| "Results"
|
| "Results"
|
||||||
| { Error: string };
|
| { Error: string };
|
||||||
|
|
||||||
|
type ApiMessage =
|
||||||
|
| { type: "device_connected"; deviceId: string }
|
||||||
|
| { type: "device_message"; deviceId: string; payload: unknown };
|
||||||
|
|
||||||
type QuizQuestion =
|
type QuizQuestion =
|
||||||
| {
|
| {
|
||||||
type: "choice";
|
type: "choice";
|
||||||
|
|
@ -59,7 +64,15 @@ type ErrorEvent = {
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PartySocketEvent = PartyStatusEvent | QuizStateEvent | ErrorEvent;
|
type DeviceLifecycleEvent =
|
||||||
|
| { 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>();
|
||||||
|
|
@ -81,6 +94,12 @@ function writeProxyOutput(socket: Socket, output: ProxyOutput) {
|
||||||
socket.write(`${JSON.stringify(output)}\n`);
|
socket.write(`${JSON.stringify(output)}\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sendApiMessage(message: ApiMessage) {
|
||||||
|
if (apiSocket.readyState !== WebSocket.OPEN) return false;
|
||||||
|
apiSocket.send(JSON.stringify(message));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function toDeviceQuestionData(quizData: QuizState): DeviceQuestionData | null {
|
function toDeviceQuestionData(quizData: QuizState): DeviceQuestionData | null {
|
||||||
if (!quizData.currentQuestion) return null;
|
if (!quizData.currentQuestion) return null;
|
||||||
const question = quizData.currentQuestion;
|
const question = quizData.currentQuestion;
|
||||||
|
|
@ -118,19 +137,23 @@ const listener = Bun.listen({
|
||||||
|
|
||||||
if ("DeviceId" in data) {
|
if ("DeviceId" in data) {
|
||||||
registerSocket(socket, data.DeviceId);
|
registerSocket(socket, data.DeviceId);
|
||||||
writeProxyOutput(socket, { ConnectPrompt: data.DeviceId });
|
if (
|
||||||
|
!sendApiMessage({ type: "device_connected", deviceId: data.DeviceId })
|
||||||
|
) {
|
||||||
|
writeProxyOutput(socket, {
|
||||||
|
Error: "API device socket not connected.",
|
||||||
|
});
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ("QuizResponse" in data) {
|
if ("QuizResponse" in data) {
|
||||||
const deviceId = socketDeviceId(socket);
|
const deviceId = socketDeviceId(socket);
|
||||||
if (!deviceId) return;
|
if (!deviceId) return;
|
||||||
apiSocket?.send(
|
sendApiMessage({
|
||||||
JSON.stringify({
|
type: "device_message",
|
||||||
type: "device_message",
|
deviceId,
|
||||||
deviceId,
|
payload: { QuizResponse: data.QuizResponse },
|
||||||
payload: { QuizResponse: data.QuizResponse },
|
});
|
||||||
}),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -156,6 +179,16 @@ apiSocket.onmessage = (e) => {
|
||||||
const socket = sockets.get(message.deviceId);
|
const socket = sockets.get(message.deviceId);
|
||||||
if (!socket) return;
|
if (!socket) return;
|
||||||
const event = message.event as PartySocketEvent;
|
const event = message.event as PartySocketEvent;
|
||||||
|
if (event.type === "device_connect_required") {
|
||||||
|
writeProxyOutput(socket, { ConnectPrompt: message.deviceId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type === "device_connected") {
|
||||||
|
writeProxyOutput(socket, "WaitingForParty");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (event.type === "error") {
|
if (event.type === "error") {
|
||||||
writeProxyOutput(socket, { Error: event.message });
|
writeProxyOutput(socket, { Error: event.message });
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ const DOT: char = char::from_u32(0b1010_0101).unwrap();
|
||||||
pub enum ViewState {
|
pub enum ViewState {
|
||||||
Loading,
|
Loading,
|
||||||
ConnectPrompt,
|
ConnectPrompt,
|
||||||
|
WaitingForParty,
|
||||||
Question,
|
Question,
|
||||||
Results,
|
Results,
|
||||||
}
|
}
|
||||||
|
|
@ -46,6 +47,7 @@ pub struct QuestionDataNet<'a> {
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub enum ProxyOutput<'a> {
|
pub enum ProxyOutput<'a> {
|
||||||
ConnectPrompt(&'a str),
|
ConnectPrompt(&'a str),
|
||||||
|
WaitingForParty,
|
||||||
Question(QuestionDataNet<'a>),
|
Question(QuestionDataNet<'a>),
|
||||||
Results,
|
Results,
|
||||||
Error(&'a str),
|
Error(&'a str),
|
||||||
|
|
@ -134,6 +136,10 @@ impl DeviceState {
|
||||||
self.question = None;
|
self.question = None;
|
||||||
self.view = ViewState::ConnectPrompt;
|
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,6 +192,13 @@ impl DeviceState {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
@ -370,6 +383,19 @@ mod tests {
|
||||||
assert_eq!(state.view_state(), ViewState::Results);
|
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]
|
#[test]
|
||||||
fn wraps_forward_across_zero() {
|
fn wraps_forward_across_zero() {
|
||||||
assert_eq!(wheel_delta(4090, 5, false), 11);
|
assert_eq!(wheel_delta(4090, 5, false), 11);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue