refactor: separate Nakama provider concerns into context, refs, and state modules
- Extracted context contract to `contexts.ts` (NakamaContextType) - Added strongly typed internal provider refs in `refs.ts` - socketRef: React.RefObject<Socket | null> - gameMetadataRef: React.RefObject<GameMetadata | null> - Added `NakamaProviderState` in `states.ts` for React-managed provider state - session, socket, matchId, matchmakerTicket - Refactored NakamaProvider to use new modular structure - Replaced scattered useState/useRef with structured internal state + refs - Updated onMatchData to use MatchDataMessage model - Replaced deprecated MutableRefObject typing with RefObject - Cleaned update patterns using `updateState` helper - Updated imports to use new models and context structure - Improved separation of responsibilities: - models = pure domain types - context = exposed provider API - refs = internal mutable runtime refs - state = provider-managed reactive state - Ensured all Nakama provider functions fully typed and consistent with TS This refactor improves clarity, type safety, and maintainability for the Nakama real-time multiplayer provider.
This commit is contained in:
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react";
|
|||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { useNakama } from "./providers/NakamaProvider";
|
import { useNakama } from "./providers/NakamaProvider";
|
||||||
import Player from "./Player";
|
import Player from "./Player";
|
||||||
import { PlayerModel } from "./models/player";
|
import { PlayerModel } from "./interfaces/models";
|
||||||
|
|
||||||
import TicTacToeBoard from "./games/tictactoe/TicTacToeBoard";
|
import TicTacToeBoard from "./games/tictactoe/TicTacToeBoard";
|
||||||
import BattleShipBoard from "./games/battleship/BattleShipBoard";
|
import BattleShipBoard from "./games/battleship/BattleShipBoard";
|
||||||
|
|||||||
15
src/games/battleship/props.ts
Normal file
15
src/games/battleship/props.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import {
|
||||||
|
Board,
|
||||||
|
PlayerModel,
|
||||||
|
} from '../../interfaces/models'
|
||||||
|
|
||||||
|
|
||||||
|
export interface BattleShipBoardProps {
|
||||||
|
boards: Record<string, Board>;
|
||||||
|
turn: number;
|
||||||
|
winner: string | null;
|
||||||
|
gameOver: boolean | null;
|
||||||
|
players: PlayerModel[];
|
||||||
|
myUserId: string | null;
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
}
|
||||||
15
src/games/tictactoe/props.ts
Normal file
15
src/games/tictactoe/props.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import {
|
||||||
|
Board,
|
||||||
|
PlayerModel,
|
||||||
|
} from '../../interfaces/models'
|
||||||
|
|
||||||
|
|
||||||
|
export interface TicTacToeBoardProps {
|
||||||
|
boards: Record<string, Board>;
|
||||||
|
turn: number;
|
||||||
|
winner: string | null;
|
||||||
|
gameOver: boolean | null;
|
||||||
|
players: PlayerModel[];
|
||||||
|
myUserId: string | null;
|
||||||
|
onCellClick: (row: number, col: number) => void;
|
||||||
|
}
|
||||||
38
src/interfaces/contexts.ts
Normal file
38
src/interfaces/contexts.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import {
|
||||||
|
Client,
|
||||||
|
Session,
|
||||||
|
Socket,
|
||||||
|
} from "@heroiclabs/nakama-js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ApiMatch,
|
||||||
|
ApiLeaderboardRecordList,
|
||||||
|
// @ts-ignore
|
||||||
|
} from "@heroiclabs/nakama-js/dist/api.gen"
|
||||||
|
|
||||||
|
import {
|
||||||
|
GameMetadata,
|
||||||
|
MatchDataMessage,
|
||||||
|
} from './models'
|
||||||
|
|
||||||
|
|
||||||
|
export interface NakamaContextType {
|
||||||
|
client: Client;
|
||||||
|
socket: Socket | null;
|
||||||
|
session: Session | null;
|
||||||
|
matchId: string | null;
|
||||||
|
|
||||||
|
loginOrRegister(username?: string): Promise<void>;
|
||||||
|
logout(): Promise<void>;
|
||||||
|
|
||||||
|
joinMatchmaker(gameMetadata: GameMetadata): Promise<string>;
|
||||||
|
exitMatchmaker(gameMetadata: GameMetadata): Promise<void>;
|
||||||
|
joinMatch(matchId: string): Promise<void>;
|
||||||
|
|
||||||
|
sendMatchData(matchId: string, op: number, data: object): void;
|
||||||
|
|
||||||
|
onMatchData(cb: (msg: MatchDataMessage) => void): void;
|
||||||
|
|
||||||
|
getLeaderboardTop(): Promise<ApiLeaderboardRecordList>;
|
||||||
|
listOpenMatches(): Promise<ApiMatch[]>;
|
||||||
|
}
|
||||||
36
src/interfaces/models.ts
Normal file
36
src/interfaces/models.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
export interface PlayerModel {
|
||||||
|
user_id: string;
|
||||||
|
username: string;
|
||||||
|
index: number;
|
||||||
|
metadata: Record<string, string>; // e.g. { symbol: "X" }
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MatchDataMessage<T = any> {
|
||||||
|
opCode: number;
|
||||||
|
data: T;
|
||||||
|
userId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Board {
|
||||||
|
grid: string[][];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GameState {
|
||||||
|
boards: Record<string, Board>;
|
||||||
|
turn: number;
|
||||||
|
winner: string | null;
|
||||||
|
gameOver: boolean;
|
||||||
|
players: PlayerModel[];
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GameMetadata {
|
||||||
|
game: string;
|
||||||
|
mode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MatchDataMessage<T = any> {
|
||||||
|
opCode: number;
|
||||||
|
data: T;
|
||||||
|
userId: string | null;
|
||||||
|
}
|
||||||
7
src/interfaces/props.ts
Normal file
7
src/interfaces/props.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import {
|
||||||
|
MatchDataMessage,
|
||||||
|
} from './models'
|
||||||
|
|
||||||
|
export interface PlayerProps {
|
||||||
|
onMatchDataCallback: (msg:MatchDataMessage) => void;
|
||||||
|
}
|
||||||
15
src/interfaces/refs.ts
Normal file
15
src/interfaces/refs.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Socket
|
||||||
|
} from "@heroiclabs/nakama-js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
GameMetadata,
|
||||||
|
} from './models'
|
||||||
|
|
||||||
|
|
||||||
|
export interface NakamaRefs {
|
||||||
|
socketRef: React.RefObject<Socket | null>;
|
||||||
|
gameMetadataRef: React.RefObject<GameMetadata | null>;
|
||||||
|
}
|
||||||
11
src/interfaces/states.ts
Normal file
11
src/interfaces/states.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import {
|
||||||
|
Session,
|
||||||
|
Socket
|
||||||
|
} from "@heroiclabs/nakama-js";
|
||||||
|
|
||||||
|
export interface NakamaProviderState {
|
||||||
|
session: Session | null;
|
||||||
|
socket: Socket | null;
|
||||||
|
matchId: string | null;
|
||||||
|
matchmakerTicket: string | null;
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export interface PlayerModel {
|
|
||||||
user_id: string;
|
|
||||||
username: string;
|
|
||||||
index: number;
|
|
||||||
metadata: Record<string, string>; // e.g. { symbol: "X" }
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useState,
|
||||||
|
useRef
|
||||||
|
} from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Client,
|
Client,
|
||||||
Session,
|
|
||||||
Socket,
|
Socket,
|
||||||
MatchmakerTicket,
|
MatchmakerTicket,
|
||||||
MatchData,
|
MatchData,
|
||||||
@@ -13,7 +19,10 @@ import {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
} from "@heroiclabs/nakama-js/dist/api.gen"
|
} from "@heroiclabs/nakama-js/dist/api.gen"
|
||||||
|
|
||||||
import React, { createContext, useContext, useState } from "react";
|
import { NakamaContextType } from "../interfaces/contexts";
|
||||||
|
import { NakamaRefs } from "../interfaces/refs";
|
||||||
|
import { NakamaProviderState } from "../interfaces/states";
|
||||||
|
import { GameMetadata, MatchDataMessage } from "../interfaces/models";
|
||||||
|
|
||||||
function getOrCreateDeviceId(): string {
|
function getOrCreateDeviceId(): string {
|
||||||
const key = "nakama.deviceId";
|
const key = "nakama.deviceId";
|
||||||
@@ -25,30 +34,6 @@ function getOrCreateDeviceId(): string {
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
type GameMetadata = {
|
|
||||||
game: string;
|
|
||||||
mode: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface NakamaContextType {
|
|
||||||
client: Client;
|
|
||||||
socket: Socket | null;
|
|
||||||
session: Session | null;
|
|
||||||
matchId: string | null;
|
|
||||||
|
|
||||||
loginOrRegister(username: string): Promise<void>;
|
|
||||||
logout(): Promise<void>;
|
|
||||||
joinMatchmaker(gameMetadata: GameMetadata): Promise<string>;
|
|
||||||
exitMatchmaker(gameMetadata: GameMetadata): Promise<void>;
|
|
||||||
joinMatch(matchId: string): Promise<void>;
|
|
||||||
|
|
||||||
sendMatchData(matchId: string, op: number, data: object): void;
|
|
||||||
|
|
||||||
onMatchData(cb: (msg: any) => void): void;
|
|
||||||
getLeaderboardTop(): Promise<ApiLeaderboardRecordList>;
|
|
||||||
listOpenMatches(): Promise<ApiMatch[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NakamaContext = createContext<NakamaContextType>(null!);
|
export const NakamaContext = createContext<NakamaContextType>(null!);
|
||||||
|
|
||||||
export function NakamaProvider({ children }: { children: React.ReactNode }) {
|
export function NakamaProvider({ children }: { children: React.ReactNode }) {
|
||||||
@@ -68,13 +53,32 @@ export function NakamaProvider({ children }: { children: React.ReactNode }) {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const gameMetadataRef = React.useRef<GameMetadata | null>(null);
|
// --------------------------------------
|
||||||
const [session, setSession] = useState<Session | null>(null);
|
// INTERNAL STATE (React state)
|
||||||
const [socket, setSocket] = useState<Socket | null>(null);
|
// --------------------------------------
|
||||||
const [matchmakerTicket, setMatchmakerTicket] = useState<string | null>(null);
|
const [internal, setInternal] = useState<NakamaProviderState>({
|
||||||
const [matchId, setMatchId] = useState<string | null>(null);
|
session: null,
|
||||||
const socketRef = React.useRef<Socket | null>(null);
|
socket: null,
|
||||||
|
matchId: null,
|
||||||
|
matchmakerTicket: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// --------------------------------------
|
||||||
|
// INTERNAL REFS (non-reactive, stable)
|
||||||
|
// --------------------------------------
|
||||||
|
const refs: NakamaRefs = {
|
||||||
|
socketRef: useRef<Socket | null>(null),
|
||||||
|
gameMetadataRef: useRef<GameMetadata | null>(null),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helpers to update internal state cleanly
|
||||||
|
function updateState(values: Partial<NakamaProviderState>) {
|
||||||
|
setInternal(prev => ({ ...prev, ...values }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------
|
||||||
|
// LOGIN FLOW
|
||||||
|
// ---------------------------------------
|
||||||
async function autoLogin() {
|
async function autoLogin() {
|
||||||
const deviceId = getOrCreateDeviceId();
|
const deviceId = getOrCreateDeviceId();
|
||||||
|
|
||||||
@@ -129,15 +133,14 @@ export function NakamaProvider({ children }: { children: React.ReactNode }) {
|
|||||||
async function loginOrRegister(username?: string) {
|
async function loginOrRegister(username?: string) {
|
||||||
// authenticate user
|
// authenticate user
|
||||||
const newSession = await getSession(username);
|
const newSession = await getSession(username);
|
||||||
setSession(newSession);
|
|
||||||
|
|
||||||
const s = client.createSocket(
|
updateState({ session: newSession });
|
||||||
import.meta.env.VITE_WS_SSL === "true",
|
|
||||||
undefined
|
const s = client.createSocket(import.meta.env.VITE_WS_SSL === "true");
|
||||||
);
|
|
||||||
await s.connect(newSession, true);
|
await s.connect(newSession, true);
|
||||||
setSocket(s);
|
|
||||||
socketRef.current = s;
|
updateState({ socket: s });
|
||||||
|
refs.socketRef.current = s;
|
||||||
|
|
||||||
console.log("[Nakama] WebSocket connected");
|
console.log("[Nakama] WebSocket connected");
|
||||||
|
|
||||||
@@ -147,9 +150,9 @@ export function NakamaProvider({ children }: { children: React.ReactNode }) {
|
|||||||
if (!matched.match_id) {
|
if (!matched.match_id) {
|
||||||
console.warn("[Nakama] Match rejected by server. Auto-requeueing...");
|
console.warn("[Nakama] Match rejected by server. Auto-requeueing...");
|
||||||
|
|
||||||
if (gameMetadataRef.current) {
|
if (refs.gameMetadataRef.current) {
|
||||||
try {
|
try {
|
||||||
await joinMatchmaker(gameMetadataRef.current);
|
await joinMatchmaker(refs.gameMetadataRef.current);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[Nakama] Requeue failed:", e);
|
console.error("[Nakama] Requeue failed:", e);
|
||||||
}
|
}
|
||||||
@@ -162,7 +165,8 @@ export function NakamaProvider({ children }: { children: React.ReactNode }) {
|
|||||||
console.log("[Nakama] MATCHED:", matched);
|
console.log("[Nakama] MATCHED:", matched);
|
||||||
try {
|
try {
|
||||||
await s.joinMatch(matched.match_id);
|
await s.joinMatch(matched.match_id);
|
||||||
setMatchId(matched.match_id);
|
updateState({ matchId: matched.match_id });
|
||||||
|
|
||||||
console.log("[Nakama] Auto-joined match:", matched.match_id);
|
console.log("[Nakama] Auto-joined match:", matched.match_id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[Nakama] Failed to join match:", err);
|
console.error("[Nakama] Failed to join match:", err);
|
||||||
@@ -170,21 +174,27 @@ export function NakamaProvider({ children }: { children: React.ReactNode }) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------
|
||||||
|
// LOGOUT
|
||||||
|
// ---------------------------------------
|
||||||
async function logout() {
|
async function logout() {
|
||||||
try {
|
try {
|
||||||
// 1) Disconnect socket if present
|
if (refs.socketRef.current) {
|
||||||
if (socketRef.current) {
|
refs.socketRef.current.disconnect(true);
|
||||||
socketRef.current.disconnect(true);
|
|
||||||
console.log("[Nakama] WebSocket disconnected");
|
console.log("[Nakama] WebSocket disconnected");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[Nakama] Error while disconnecting socket:", err);
|
console.warn("[Nakama] Error while disconnecting socket:", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Clear state
|
updateState({
|
||||||
setSocket(null);
|
session: null,
|
||||||
socketRef.current = null;
|
socket: null,
|
||||||
setSession(null);
|
matchId: null,
|
||||||
|
matchmakerTicket: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
refs.socketRef.current = null;
|
||||||
|
|
||||||
console.log("[Nakama] Clean logout completed");
|
console.log("[Nakama] Clean logout completed");
|
||||||
}
|
}
|
||||||
@@ -193,11 +203,10 @@ export function NakamaProvider({ children }: { children: React.ReactNode }) {
|
|||||||
// MATCHMAKING
|
// MATCHMAKING
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
async function joinMatchmaker(gameMetadata: GameMetadata) {
|
async function joinMatchmaker(gameMetadata: GameMetadata) {
|
||||||
const socket = socketRef.current;
|
const socket = refs.socketRef.current;
|
||||||
const game = gameMetadata.game;
|
if (!socket) throw new Error("Socket missing");
|
||||||
const mode = gameMetadata.mode;
|
|
||||||
if (!socket) throw new Error("socket missing");
|
|
||||||
|
|
||||||
|
const { game, mode } = gameMetadata;
|
||||||
if (!game || game.trim() === "") {
|
if (!game || game.trim() === "") {
|
||||||
throw new Error("Matchmaking requires a game name");
|
throw new Error("Matchmaking requires a game name");
|
||||||
}
|
}
|
||||||
@@ -213,30 +222,35 @@ export function NakamaProvider({ children }: { children: React.ReactNode }) {
|
|||||||
{ game, mode }
|
{ game, mode }
|
||||||
);
|
);
|
||||||
|
|
||||||
gameMetadataRef.current = { game, mode };
|
refs.gameMetadataRef.current = { game, mode };
|
||||||
setMatchmakerTicket(ticket.ticket);
|
|
||||||
|
updateState({ matchmakerTicket: ticket.ticket });
|
||||||
|
|
||||||
return ticket.ticket;
|
return ticket.ticket;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exitMatchmaker(gameMetadata: GameMetadata) {
|
async function exitMatchmaker() {
|
||||||
const socket = socketRef.current;
|
const socket = refs.socketRef.current;
|
||||||
const game = gameMetadata.game;
|
const { matchmakerTicket } = internal;
|
||||||
const mode = gameMetadata.mode;
|
|
||||||
if (!socket) throw new Error("socket missing");
|
|
||||||
|
|
||||||
console.log(`[Nakama] Exiting Matchmaking... game="${game}" mode="${mode}"`);
|
if (!socket) throw new Error("Socket missing");
|
||||||
if (matchmakerTicket) await socket.removeMatchmaker(matchmakerTicket);
|
|
||||||
setMatchmakerTicket(null);
|
if (matchmakerTicket) {
|
||||||
|
await socket.removeMatchmaker(matchmakerTicket);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateState({ matchmakerTicket: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
// EXPLICIT MATCH JOIN
|
// EXPLICIT MATCH JOIN
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
async function joinMatch(id: string) {
|
async function joinMatch(id: string) {
|
||||||
if (!socket) throw new Error("socket missing");
|
if (!internal.socket) throw new Error("Socket missing");
|
||||||
await socket.joinMatch(id);
|
|
||||||
setMatchId(id);
|
await internal.socket.joinMatch(id);
|
||||||
|
|
||||||
|
updateState({ matchId: id });
|
||||||
console.log("[Nakama] Joined match", id);
|
console.log("[Nakama] Joined match", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,18 +258,19 @@ export function NakamaProvider({ children }: { children: React.ReactNode }) {
|
|||||||
// MATCH STATE SEND
|
// MATCH STATE SEND
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
function sendMatchData(matchId: string, op: number, data: object) {
|
function sendMatchData(matchId: string, op: number, data: object) {
|
||||||
if (!socket) return;
|
if (!internal.socket) return;
|
||||||
|
|
||||||
console.log("[Nakama] Sending match state:", matchId, op, data);
|
console.log("[Nakama] Sending match state:", matchId, op, data);
|
||||||
socket.sendMatchState(matchId, op, JSON.stringify(data));
|
internal.socket.sendMatchState(matchId, op, JSON.stringify(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
// MATCH DATA LISTENER
|
// MATCH DATA LISTENER
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
function onMatchData(cb: (msg: any) => void) {
|
function onMatchData(cb: (msg: MatchDataMessage) => void) {
|
||||||
if (!socket) return;
|
if (!internal.socket) return;
|
||||||
|
|
||||||
socket.onmatchdata = (m: MatchData) => {
|
internal.socket.onmatchdata = (m: MatchData) => {
|
||||||
const decoded = JSON.parse(new TextDecoder().decode(m.data));
|
const decoded = JSON.parse(new TextDecoder().decode(m.data));
|
||||||
cb({
|
cb({
|
||||||
opCode: m.op_code,
|
opCode: m.op_code,
|
||||||
@@ -265,36 +280,43 @@ export function NakamaProvider({ children }: { children: React.ReactNode }) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------
|
||||||
|
// LEADERBOARD + LIST MATCHES
|
||||||
|
// ---------------------------------------
|
||||||
async function getLeaderboardTop(): Promise<ApiLeaderboardRecordList> {
|
async function getLeaderboardTop(): Promise<ApiLeaderboardRecordList> {
|
||||||
if (!session) return [];
|
if (!internal.session) return [];
|
||||||
|
|
||||||
return await client.listLeaderboardRecords(
|
return await client.listLeaderboardRecords(
|
||||||
session,
|
internal.session,
|
||||||
"tictactoe",
|
"tictactoe",
|
||||||
[],
|
[],
|
||||||
10 // top 10
|
10 // top 10
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listOpenMatches(): Promise<ApiMatch[]> {
|
async function listOpenMatches(): Promise<ApiMatch[]> {
|
||||||
if (!session) {
|
if (!internal.session) {
|
||||||
console.warn("[Nakama] listOpenMatches called before login");
|
console.warn("[Nakama] listOpenMatches called before login");
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await client.listMatches(session, 10);
|
const result = await client.listMatches(internal.session, 10);
|
||||||
|
|
||||||
console.log("[Nakama] Open matches:", result.matches);
|
console.log("[Nakama] Open matches:", result.matches);
|
||||||
|
|
||||||
return result.matches ?? [];
|
return result.matches ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------
|
||||||
|
// PROVIDER VALUE
|
||||||
|
// ---------------------------------------
|
||||||
return (
|
return (
|
||||||
<NakamaContext.Provider
|
<NakamaContext.Provider
|
||||||
value={{
|
value={{
|
||||||
client,
|
client,
|
||||||
session,
|
session: internal.session,
|
||||||
socket,
|
socket: internal.socket,
|
||||||
matchId,
|
matchId: internal.matchId,
|
||||||
|
|
||||||
loginOrRegister,
|
loginOrRegister,
|
||||||
logout,
|
logout,
|
||||||
joinMatchmaker,
|
joinMatchmaker,
|
||||||
|
|||||||
Reference in New Issue
Block a user