heroiclabs / nakama-js

JavaScript client for Nakama server written in TypeScript.
https://heroiclabs.com/docs/nakama/client-libraries/javascript/
Apache License 2.0
182 stars 54 forks source link

SyntaxError: JSON Parse error: Unexpected identifier "continuation" #172

Open technicallyty opened 9 months ago

technicallyty commented 9 months ago

I'm using "@heroiclabs/nakama-js": "^2.6.1" to build a client in TypeScript. I use sockets to make calls to RPC's on the Nakama backend. For some reason, I randomly get these errors spit into my console:

SyntaxError: JSON Parse error: Unexpected identifier "continuation"
      at /Users/REDACTED/client/node_modules/@heroiclabs/nakama-js/dist/nakama-js.esm.mjs:3013:24
3008 |     return this._socket.onmessage;
3009 |   }
3010 |   set onMessage(value) {
3011 |     if (value) {
3012 |       this._socket.onmessage = (evt) => {
3013 |         const message = JSON.parse(evt.data);
                            ^

my Go-based server is logging: (using github.com/heroiclabs/nakama-common v1.27.0)

{"level":"debug","ts":"2023-09-19T21:37:11.533Z","caller":"server/session_ws.go:204","msg":"Error reading message from client","uid":"8fa3b6e6-695c-475a-96ca-e3afcba467b7","sid":"a8198a91-5734-11ee-9dcd-006100a0eb06","error":"websocket: continuation after FIN"}

It's really unclear why this is happening, and the error message doesn't provide much context to help me out.

lugehorsam commented 9 months ago

Hey @technicallyty, "continuation after FIN" happens when data or packets continue to be sent after a FIN has been issued. It's likely there is some half-closed connection occurring between the sockets. It's not something we've seen before. Is this something you are reproducing locally?

technicallyty commented 9 months ago

yeah, im just testing out RPC calls to a local nakama instance. fwiw, ive gotten rid of this error. but now the socket just... stops working after some time? i can just post my integration code here:

import {Client, Session, Socket} from "@heroiclabs/nakama-js";
import {ReadMonsterStatusResponse} from "./types.ts";
import {ApiRpc} from "@heroiclabs/nakama-js/dist/api.gen";

interface NakamaIdentifier {
    DeviceID: string;
    Session: Session;
}

class Nakama {
    private client: Client;
    private readonly useSSL: boolean;
    private identifiers: Map<string, NakamaIdentifier>;

    constructor() {
        this.client = new Client("defaultkey", "localhost", "7350");
        this.useSSL = false; // Initialize useSSL with a default value.
        this.identifiers = new Map<string, NakamaIdentifier>;
    }

    async authenticate(userName: string) : Promise<any> {
        const email: string = userName + "@gmail.com";
        const pass: string = "FooBar1234567890!";

        let session: Session = await this.client.authenticateEmail(
            email,
            pass,
            true,
            userName
        );
        let sock: Socket = this.client.createSocket(this.useSSL, false);
        await sock.connect(session, true);
        const payload: string = JSON.stringify({ persona_tag: userName});
        await sock.rpc(
            "nakama/claim-persona",
            payload
        ).catch(e => {
            console.error(e)
        })
        this.identifiers[userName] = {DeviceID: email, Session: session}
        sock.disconnect(false);
    }

    async status(personaTag: string) : Promise<ReadMonsterStatusResponse> {
        const payload: string = JSON.stringify({Owner: personaTag});
        let response = await this.makeRPCCall(personaTag, payload, "read-monster")
        return JSON.parse(response.payload!);
    }

    async recall(personaTag: string) : Promise<void> {
        const payload: string = JSON.stringify({});
        await this.makeRPCCall(personaTag, payload, "tx-recall-monster");
    }

    async adventure(personaTag: string, difficulty: string) : Promise<void> {
        const payload: string = JSON.stringify({Difficulty: difficulty});
        await this.makeRPCCall(personaTag, payload, "tx-adventure");
    }

    async nameMonster(personaTag: string, monsterName: string) : Promise<void> {
        const payload: string = JSON.stringify({ Name: monsterName });
        await this.makeRPCCall(personaTag, payload, "tx-name-monster");
    }

    async feedMonster(personaTag: string) : Promise<void> {
        const payload: string = JSON.stringify({});
        await this.makeRPCCall(personaTag, payload, "tx-feed-monster");
    }

    async claimMonster(personaTag: string) : Promise<void> {
        const payload: string = JSON.stringify({});
        await this.makeRPCCall(personaTag, payload, "tx-claim-monster");
    }

    async checkPersona(personaTag: string) : Promise<void> {
        const payload: string = JSON.stringify({ persona_tag: personaTag});
        await this.makeRPCCall(personaTag, payload,"nakama/show-persona")
    }

    async makeRPCCall(personaTag: string, payload: string, rpcName: string) : Promise<ApiRpc> {
        let id: NakamaIdentifier = this.identifiers[personaTag];
        let sock: Socket = this.client.createSocket(this.useSSL, false);
        await sock.connect(id.Session, false).catch(e => {
            console.error("ERROR CONNECTING TO SOCKET")
            console.error(e);
        });
        let response = await sock.rpc(
            rpcName,
            payload,
        )
        console.debug(response.http_key)
        console.debug(response.id)
        console.debug(response.payload)
        sock.disconnect(false);
        return response;
    }
}

export default Nakama; // Export the Nakama class itself.
lugehorsam commented 9 months ago

I recommend making your RPCs from the client object instead of the socket. Your pattern appears to be more of a request-response model and when you use the client you won't need to setup and tear down the socket anymore -- that's just incurring unnecessary overhead and complexity.

technicallyty commented 9 months ago

ok i switched to RPC and thats fine. but now im using a socket to connect to a match and listen for things. and i randomly get that "continuation" error as described above. its still not clear why thats happening

cc @lugehorsam

lugehorsam commented 9 months ago

@technicallyty could you share your connection open and close logic and how long your typical match is?

technicallyty commented 9 months ago

@technicallyty could you share your connection open and close logic and how long your typical match is?

the match is never intended to close, it should be alive as long as our nakama instance is running.

           this.receiptChannel = this.client.createSocket(false, false);
            this.receiptChannel.onmatchdata = result => {
                console.log("received match data:")
                console.log(result);
            }
            this.receiptChannel.ondisconnect = evt => {
                console.log("disconnecting socket...");
                console.log(evt);
            }
            this.receiptChannel.onerror = e => {
                console.log("received error...")
                console.log(e);
            }
            let sesh = await this.receiptChannel.connect(session, false);
            let result = await this.client.listMatches(sesh);
            let matchId = result.matches[0].match_id;
            await this.receiptChannel.joinMatch(matchId);
lugehorsam commented 9 months ago

As an aside, I would be careful about designing a system where matches open and run indefinitely without closing. Your server will OOM if you do that.

How long does your match typically run before you see this issue?

smsunarto commented 9 months ago

As an aside, I would be careful about designing a system where matches open and run indefinitely without closing. Your server will OOM if you do that.

How long does your match typically run before you see this issue?

Is there a reason why this is the case? Is there some sort of memory leak within the match runtime?

novabyte commented 9 months ago

@smsunarto There is no memory leak in the server but if you have new matches created continuously and keep them alive without recycling them back into use with players you'll exhaust available memory.

This does not have anything to do with the intrinsic design of the multiplayer engine; you must consider how to utilize the finite resources of the hardware like with any server system.

technicallyty commented 9 months ago

How long does your match typically run before you see this issue?

usually 10-15 seconds

smsunarto commented 9 months ago

@smsunarto There is no memory leak in the server but if you have new matches created continuously and keep them alive without recycling them back into use with players you'll exhaust available memory.

This does not have anything to do with the intrinsic design of the multiplayer engine; you must consider how to utilize the finite resources of the hardware like with any server system.

Ah yes, to clarify, we only have 1 ongoing match at all times; we're basically treating it like a singleton match.

lugehorsam commented 3 months ago

@technicallyty @smsunarto is this still an issue for you and if so, where are you running the server when you see the error? Is it local?