diff --git a/src/tictactoe/TicTacToe.tsx b/src/tictactoe/TicTacToe.tsx
index 3396a7f..bab7d52 100644
--- a/src/tictactoe/TicTacToe.tsx
+++ b/src/tictactoe/TicTacToe.tsx
@@ -19,8 +19,8 @@ export default function TicTacToe() {
joinMatchmaker,
joinMatch,
onMatchData,
- socket,
- sendMatchData
+ onMatchmakerMatched,
+ sendMatchData,
} = useNakama();
// ------------------------------------------
@@ -42,15 +42,14 @@ export default function TicTacToe() {
}
});
- // When matchmaker finds a match
- socket!.onmatchmakermatched = async (matched) => {
+ onMatchmakerMatched(async (matched) => {
console.log("Matched:", matched);
const fullMatchId = matched.match_id;
setMatchId(fullMatchId);
await joinMatch(fullMatchId);
- };
+ })
}
// ------------------------------------------
@@ -77,7 +76,7 @@ export default function TicTacToe() {
{!matchId && (
<>
setUsername(e.target.value)}
/>
diff --git a/src/tictactoe/providers/NakamaProvider.tsx b/src/tictactoe/providers/NakamaProvider.tsx
index 2fac9f2..fca4671 100644
--- a/src/tictactoe/providers/NakamaProvider.tsx
+++ b/src/tictactoe/providers/NakamaProvider.tsx
@@ -6,90 +6,186 @@ import {
MatchmakerTicket,
Match,
MatchData,
+ MatchmakerMatched,
} from "@heroiclabs/nakama-js";
-import React, { createContext, useContext, useState } from "react";
+import React, {
+ createContext,
+ useContext,
+ useEffect,
+ useRef,
+ useState,
+} from "react";
+
+// ---------------------------------------------
+// UNIQUE DEVICE ID FOR BROWSER
+// ---------------------------------------------
+function getOrCreateDeviceId(): string {
+ const key = "nakama.deviceId";
+ let id = localStorage.getItem(key);
+
+ if (!id) {
+ id = crypto.randomUUID();
+ localStorage.setItem(key, id);
+ }
+
+ return id;
+}
+
+// ---------------------------------------------
+// CONTEXT TYPE
+// ---------------------------------------------
export interface NakamaContextType {
client: Client;
socket: Socket | null;
session: Session | null;
loginOrRegister: (username: string) => Promise;
+
joinMatchmaker: (mode: string) => Promise;
leaveMatchmaker: (ticket: string) => Promise;
+
joinMatch: (matchId: string) => Promise;
+
sendMatchData: (matchId: string, opCode: number, data: object) => void;
+
onMatchData: (
cb: (msg: { opCode: number; data: any; userId: string | null }) => void
) => void;
+
+ onMatchmakerMatched: (cb: (result: MatchmakerMatched) => void) => void;
}
export const NakamaContext = createContext(null);
+// ---------------------------------------------
+// PROVIDER IMPLEMENTATION
+// ---------------------------------------------
export function NakamaProvider({ children }: { children: React.ReactNode }) {
- const [client] = useState(
- () => new Client(
- "defaultkey",
- "127.0.0.1",
- "7350"
- )
+ // Use "defaultkey", "127.0.0.1", "7350" — match your given client
+ const [client] = useState(
+ () =>
+ new Client(
+ "defaultkey",
+ "127.0.0.1",
+ "7350",
+ false, // use SSL?
+ )
);
- const [socket, setSocket] = useState(null);
const [session, setSession] = useState(null);
+ const [socket, setSocket] = useState(null);
- // -----------------------
- // LOGIN / REGISTER
- // -----------------------
+ // ---------------------------------------------
+ // CALLBACK REGISTRIES (React-safe)
+ // ---------------------------------------------
+ const matchDataCallbacks = useRef<
+ ((msg: { opCode: number; data: any; userId: string | null }) => void)[]
+ >([]);
+
+ const matchmakerMatchedCallbacks = useRef<
+ ((result: MatchmakerMatched) => void)[]
+ >([]);
+
+ // Register match data listener
+ function onMatchData(
+ cb: (msg: { opCode: number; data: any; userId: string | null }) => void
+ ) {
+ matchDataCallbacks.current.push(cb);
+ }
+
+ // Register matchmaker matched listener
+ function onMatchmakerMatched(cb: (result: MatchmakerMatched) => void) {
+ matchmakerMatchedCallbacks.current.push(cb);
+ }
+
+ // ---------------------------------------------
+ // SETUP SOCKET EVENT LISTENERS WHEN SOCKET IS READY
+ // ---------------------------------------------
+ useEffect(() => {
+ if (!socket) return;
+
+ // MATCH DATA
+ socket.onmatchdata = (msg: MatchData) => {
+ const raw = msg.data;
+ let decoded: any;
+
+ try {
+ decoded = JSON.parse(new TextDecoder().decode(raw));
+ } catch {
+ decoded = raw;
+ }
+
+ matchDataCallbacks.current.forEach((cb) =>
+ cb({
+ opCode: msg.op_code,
+ data: decoded,
+ userId: msg.presence?.user_id || null,
+ })
+ );
+ };
+
+ // MATCHMAKER MATCHED
+ socket.onmatchmakermatched = (result) => {
+ matchmakerMatchedCallbacks.current.forEach((cb) => cb(result));
+ };
+
+ return () => {
+ // Cleanup (not strictly needed, but clean React practice)
+ socket.onmatchdata = undefined as any;
+ socket.onmatchmakermatched = undefined as any;
+ };
+ }, [socket]);
+
+ // ---------------------------------------------
+ // LOGIN OR REGISTER
+ // ---------------------------------------------
async function loginOrRegister(username: string): Promise {
try {
- const s = await client.authenticateDevice(username, true);
+ const deviceId = getOrCreateDeviceId();
+
+ // authenticateDevice(id, create, username)
+ const s = await client.authenticateDevice(deviceId, true, username);
setSession(s);
- const newSocket = new DefaultSocket(
- client.host,
- client.port,
- false, // useSSL
- false // verbose
- );
-
+ const newSocket = client.createSocket(false, false);
await newSocket.connect(s, true);
setSocket(newSocket);
- console.log("[Nakama] Connected WS");
+ console.log("[Nakama] Connected via WebSocket");
return s;
} catch (err) {
- console.error("[Nakama] Login error", err);
+ console.error("[Nakama] Login error:", err);
throw err;
}
}
- // -----------------------
+ // ---------------------------------------------
// MATCHMAKING
- // -----------------------
+ // ---------------------------------------------
async function joinMatchmaker(mode: string): Promise {
if (!socket) throw new Error("Socket not ready.");
- const ticket = await socket.addMatchmaker(
- `+mode:"${mode}"`, // query
- 2, // min players
- 2, // max players
- { mode } // string properties
+ const ticket: MatchmakerTicket = await socket.addMatchmaker(
+ `+mode:"${mode}"`, // query
+ 2, // min count
+ 2, // max count
+ { mode } // stringProperties
);
- console.log("[Nakama] Matchmaker ticket:", ticket.ticket);
+ console.log("[Nakama] Ticket:", ticket.ticket);
return ticket.ticket;
}
- async function leaveMatchmaker(ticket: string): Promise {
+ async function leaveMatchmaker(ticket: string) {
if (socket) {
await socket.removeMatchmaker(ticket);
}
}
- // -----------------------
+ // ---------------------------------------------
// JOIN MATCH
- // -----------------------
+ // ---------------------------------------------
async function joinMatch(matchId: string): Promise {
if (!socket) throw new Error("Socket not connected.");
const match = await socket.joinMatch(matchId);
@@ -97,41 +193,17 @@ export function NakamaProvider({ children }: { children: React.ReactNode }) {
return match;
}
- // -----------------------
+ // ---------------------------------------------
// SEND MATCH DATA
- // -----------------------
- function sendMatchData(matchId: string, opCode: number, data: object = {}): void {
+ // ---------------------------------------------
+ function sendMatchData(matchId: string, opCode: number, data: object) {
if (!socket) return;
socket.sendMatchState(matchId, opCode, JSON.stringify(data));
}
- // -----------------------
- // LISTENERS
- // -----------------------
- function onMatchData(
- cb: (msg: { opCode: number; data: any; userId: string | null }) => void
- ) {
- if (!socket) return;
-
- socket.onmatchdata = (msg: MatchData) => {
- const raw = msg.data;
- let decoded: any = null;
-
- try {
- decoded = JSON.parse(new TextDecoder().decode(raw));
- } catch {
- decoded = raw; // fallback to raw binary
- }
-
- cb({
- opCode: msg.op_code,
- data: decoded,
- userId: msg.presence?.user_id ?? null,
- });
- };
- }
-
-
+ // ---------------------------------------------
+ // PROVIDER VALUE
+ // ---------------------------------------------
return (
{children}
@@ -151,10 +224,11 @@ export function NakamaProvider({ children }: { children: React.ReactNode }) {
);
}
+// ---------------------------------------------
+// USE HOOK
+// ---------------------------------------------
export function useNakama(): NakamaContextType {
const ctx = useContext(NakamaContext);
- if (!ctx) {
- throw new Error("useNakama must be used inside a NakamaProvider");
- }
+ if (!ctx) throw new Error("useNakama must be inside a NakamaProvider");
return ctx;
}