스트림 #

스트림은 Nakama의 내부 실시간 라우팅 및 전달을 완전히 제어할 수 있는 강력한 기능입니다.

Nakama의 실시간 메시지 라우팅 및 전달 하위 시스템은 스트림으로 구성됩니다. 스트림을 통해 특정 메시지 유형에 관심이 있는 클라이언트를 결합하고 Nakama의 내부 구성 요소를 통해 연관된 사용자에게 메시지를 전달합니다.

클라이언트는 스트림에서 메시지와 데이터를 수신할 수 있지만, 직접 가입, 탈퇴하거나 데이터를 전송할 수 없습니다. 이 기능은 서버 코드 런타임 에서만 사용할 수 있습니다.

서버 내에 있는 모든 상위 수준의 실시간 기능(채팅 채널, 멀티플레이어, 알림 등)은 스트림 위의 기능으로 구성됩니다. 이 기능을 사용하기 위해서 스트림을 이해하고 사용하지 않아도 됩니다.

스트림의 구조 #

스트림은 다음 두 가지 요소로 정의됩니다: 스트림 식별자현재 상태 목록.

스트림 식별자 #

모든 스트림에는 고유한 ID가 있습니다. 이것은 사용자를 스트림에 배치하고 메시지 전달을 위해 스트림을 배치할 때 사용됩니다. 스트림 ID에는 4개의 필드가 있습니다:

  • 모드는 스트림의 유형을 표시합니다. 예를 들어, 채팅 채널은 이름이 다르지만 모드는 같습니다.
  • 주체에는 사용자 ID와 같은 주요한 스트림 주체가 포함됩니다. 모드는 유일한 필수 필드입니다.
  • 설명자는 보조 ID입니다. 스트림이 두 사용자 간의 직접 채팅 등 사용자 또는 그룹의 쌍으로 구성될 때 사용됩니다.
  • 레이블은 메타 정보로 사용될 수 있는 문자열을 저장합니다. 이름으로 생성된 채팅방은 레이블 필드를 사용합니다.

현재 상태 목록 #

스트림은 한 조의 온라인 사용자에 어드레스를 지정하고 그들에게 메시지를 전달하기 위해서 사용됩니다. 각각의 스트림은 사용자가 연결된 소켓으로 사용자를 고유하게 식별하는 현재 상태 목록을 유지합니다. 서버가 스트림으로 메시지를 전송하면 현재 상태로 식별되는 모든 클라이언트에 메시지가 전달됩니다.

지속성 및 메시지 내역 #

현재 상태는 선택적 지속성 플래그로 표시할 수 있습니다. 서버는 스트림으로 전달된 메시지를 처리할 때 이 플래그를 참고하여 메시지 데이터를 데이터베이스에 저장할지 결정합니다. 실시간 채팅 기능은 이 플래그를 사용하여 클라이언트가 메시지 내역을 요청할 수 있도록 메시지를 저장해야 할지 결정합니다.

숨겨진 스트림 구성원 #

새로운 사용자가 가입하거나 기존의 사용자가 스트림을 탈퇴할 경우 스트림은 현재 상태 이벤트를 생성하여 현재 스트림에 있는 모든 사용자에게 알림을 제공합니다. 현재 상태는 선택이 가능한 숨겨진 플래그로 표시할 수 있습니다. true로 설정될 경우, 서버는 사용자가 가입하거나 탈퇴할 때 현재 상태 이벤트를 생성하지 않습니다.

숨겨진 현재 상태는 완전한 스트림 구성원이기 때문에 데이터와 현재 상태 이벤트를 정상적으로 수신합니다.

스트림 데이터 수신 #

클라이언트는 이벤트 핸들러를 등록하여 소켓을 통해 수신되는 스트림 데이터 개체를 사용할 수 있습니다. 핸들러 함수는 스트림 데이터 메시지마다 1회 호출됩니다.

Client
1
2
3
4
socket.onstreamdata = (streamdata) => {
	console.log("Received data from stream: %o", streamdata.stream.subject);
	console.log("Data content: %@.", streamdata.data);
};
Client
1
2
3
4
5
socket.ReceivedStreamState += stream =>
{
	Console.WriteLine("Received data from stream: '{0}'", stream.Stream.Subject);
	Console.WriteLine("Data content: {0}", stream.State);
};
Client
1
2
3
4
5
socket.ReceivedStreamState += stream =>
{
		Debug.LogFormat("Received data from stream: '{0}'", stream.Stream.Subject);
		Debug.LogFormat("Data content: {0}", stream.State);
};
Client
1
2
3
4
5
6
7
// add listener to header of your class: NRtDefaultClientListener listener;
rtClient->setListener(&listener);
listener.onStreamData([](const NStreamData& data)
{
		CCLOG("Received data from stream: %s", data.stream.subject.c_str());
		CCLOG("Data content: %s", data.data.c_str());
});
Client
1
2
3
4
socket.onstreamdata = function (streamdata) {
	cc.log("Received data from stream:", streamdata.stream.subject);
	cc.log("Data content:", streamdata.data);
};
Client
1
2
3
4
5
6
7
// add listener to header of your class: NRtDefaultClientListener listener;
rtClient->setListener(&listener);
listener.onStreamData([](const NStreamData& data)
{
		cout << "Received data from stream: " << data.stream.subject << endl;
		cout << "Data content: " << data.data << endl;
});
Client
1
2
3
4
5
6
7
SocketListener listener = new AbstractSocketListener() {
	@Override
	public void onStreamData(final StreamData data) {
		System.out.println("Received data from stream: " + data.getStream().getSubject());
		System.out.println("Data content: " + data.getData());
	}
};
Client
1
2
3
4
5
6
7
func _ready():
	# First, setup the socket as explained in the authentication section.
	socket.connect("received_stream_state", self, "_on_stream_state")

func _on_stream_state(p_state : NakamaRTAPI.StreamData):
	print("Received data from stream: %s" % [p_state.stream])
	print("Data: %s" % [parse_json(p_state.state)])
Client
1
2
3
local result = socket.on_streamdata(function(message)
		pprint(message)
end)

스트림 현재 상태 이벤트 수신 #

새로운 현재 상태가 스트림에 추가되거나 기존의 현재 상태가 삭제될 경우, 서버는 현재 상태 이벤트를 현재 스트림에 있는 모든 사용자에게 전달합니다.

Client
1
2
3
4
5
6
7
8
9
socket.onstreampresence = (streampresence) => {
	console.log("Received presence event for stream: %o", streampresence.id);
	streampresence.joins.forEach((join) => {
		console.log("New user joined: %o", join.user_id);
	});
	streampresence.leaves.forEach((leave) => {
		console.log("User left: %o", leave.user_id);
	});
};
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
socket.ReceivedStreamPresence += presenceEvent =>
{
		Console.WriteLine("Received data from stream: '{0}'", presenceEvent.Stream.Subject);
		foreach (var joined in presenceEvent.Joins)
		{
				Console.WriteLine("Joined: {0}", joined);
		}
		foreach (var left in presenceEvent.Leaves)
		{
				Console.WriteLine("Left: {0}", left);
		}
};
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
socket.ReceivedStreamPresence += presenceEvent =>
{
		Debug.LogFormat("Received data from stream: '{0}'", presenceEvent.Stream.Subject);
		foreach (var joined in presenceEvent.Joins)
		{
				Debug.LogFormat("Joined: {0}", joined);
		}
		foreach (var left in presenceEvent.Leaves)
		{
				Debug.LogFormat("Left: {0}", left);
		}
};
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// add listener to header of your class: NRtDefaultClientListener listener;
rtClient->setListener(&listener);
listener.onStreamPresence([](const NStreamPresenceEvent& presence)
{
		CCLOG("Received presence event for stream: %s", presence.stream.subject.c_str());
		for (const NUserPresence& userPresence : presence.joins)
		{
			CCLOG("New user joined: %s", userPresence.user_id.c_str());
		}
		for (const NUserPresence& userPresence : presence.leaves)
		{
			CCLOG("User left: %s", userPresence.user_id.c_str());
		}
});
Client
1
2
3
4
5
6
7
8
9
socket.onstreampresence = function(streampresence) {
	cc.log("Received presence event for stream:", streampresence.id);
	streampresence.joins.forEach(function(join) {
		cc.log("New user joined:", join.user_id);
	});
	streampresence.leaves.forEach(function(leave) {
		cc.log("User left:", leave.user_id);
	});
};
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// add listener to header of your class: NRtDefaultClientListener listener;
rtClient->setListener(&listener);
listener.onStreamPresence([](const NStreamPresenceEvent& presence)
{
		cout << "Received presence event for stream: " << presence.stream.subject << endl;
		for (const NUserPresence& userPresence : presence.joins)
		{
			cout << "New user joined: " << userPresence.user_id << endl;
		}
		for (const NUserPresence& userPresence : presence.leaves)
		{
			cout << "User left: " << userPresence.user_id << endl;
		}
});
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
SocketListener listener = new AbstractSocketListener() {
	@Override
	public void onStreamPresence(final StreamPresenceEvent presence) {
		System.out.println("Received presence event for stream: " + presence.getStream().getSubject());

		for (UserPresence userPresence : presence.getJoins()) {
			System.out.println("User ID: " + userPresence.getUserId() + " Username: " + userPresence.getUsername() + " Status: " + userPresence.getStatus());
		}

		for (UserPresence userPresence : presence.getLeaves()) {
			System.out.println("User ID: " + userPresence.getUserId() + " Username: " + userPresence.getUsername() + " Status: " + userPresence.getStatus());
		}
	}
};
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func _ready():
	# First, setup the socket as explained in the authentication section.
	socket.connect("received_stream_presence", self, "_on_stream_presence")

func _on_stream_presence(p_presence : NakamaRTAPI.StreamPresenceEvent):
	print("Received presences on stream: %s" % [p_presence.stream])
	for p in p_presence.joins:
		print("User ID: %s, Username: %s, Status: %s" % [p.user_id, p.username, p.status])
	for p in p_presence.leaves:
		print("User ID: %s, Username: %s, Status: %s" % [p.user_id, p.username, p.status])
Client
1
2
3
4
local result = socket.on_streampresence(function(message)
		pprint(message.joins)
		pprint(message.leaves)
end)

숨겨진 현재 상태는 현재 상태 이벤트를 생성하지 않고 이벤트 핸들러에 의해 수신된 결과에 표시되지 않습니다.

스트림 가입 #

서버는 모든 스트림에 사용자를 배치할 수 있습니다. 사용자를 스트림에 추가하려면 서버에는 사용자 ID, 사용자 현재 세션의 고유한 세션 ID, 배치할 스트림에 대한 정보가 필요합니다.

예를 들어, 사용자 지정 스트림에서 호출하는 사용자를 배치하는 RPC 함수를 등록할 수 있습니다.

Server
1
2
3
4
5
6
7
local function join(context, _)
	local stream_id = { mode = 123, label = "my custom stream" }
	local hidden = false
	local persistence = false
	nk.stream_user_join(context.user_id, context.session_id, stream_id, hidden, persistence)
end
nk.register_rpc(join, "join")
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func JoinStream(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
	userID, ok := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)
	if !ok {
		// If user ID is not found, RPC was called without a session token.
		return "", errors.New("Invalid context")
	}
	sessionID, ok := ctx.Value(runtime.RUNTIME_CTX_SESSION_ID).(string)
	if !ok {
		// If session ID is not found, RPC was not called over a connected socket.
		return "", errors.New("Invalid context")
	}

	mode := 123
	hidden := false
	persistence := false
	if _, err := nk.StreamUserJoin(mode, "", "", "label", userID, sessionID, hidden, persistence, ""); err != nil {
		return "", err
	}

	return "Success", nil
}

// Register as RPC function, this call should be in InitModule.
if err := initializer.RegisterRpc("join", JoinStream); err != nil {
	logger.Error("Unable to register: %v", err)
	return err
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let joinFunction: nkruntime.RpcFunction = function(ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string) {
		let streamId: nkruntime.Stream = {
				mode: 123,
				label: 'my custom stream',
		};
		let hidden = false;
		let persistence = false;
		nk.streamUserJoin(ctx.userId, ctx.sessionId, streamId, hidden, persistence);
}

// Register as RPC function, this call should be in InitModule.
initializer.registerRpc('join', joinFunction);

사용자와 세션이 이미 스트림의 구성원인 경우, 작동 상태는 no-op입니다.

스트림 탈퇴 #

스트림 탈퇴도 서버에서 제어됩니다. 사용자를 스트림에서 제거하려면 서버에는 사용자 ID, 사용자 현재 세션의 고유한 세션 ID, 제거할 스트림에 대한 정보가 필요합니다.

예를 들어, 사용자 지정 스트림에서 호출하는 사용자를 제거하는 RPC 함수를 등록할 수 있습니다.

Server
1
2
3
4
5
local function leave(context, _)
	local stream_id = { mode = 123, label = "my custom stream" }
	nk.stream_user_leave(context.user_id, context.session_id, stream_id)
end
nk.register_rpc(leave, "leave")
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// RPC Code
func LeaveStream(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
	userID, ok := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)
	if !ok {
		// If user ID is not found, RPC was called without a session token.
		return "", errors.New("Invalid context")
	}
	sessionID, ok := ctx.Value(runtime.RUNTIME_CTX_SESSION_ID).(string)
	if !ok {
		// If session ID is not found, RPC was not called over a connected socket.
		return "", errors.New("Invalid context")
	}

	if err := nk.StreamUserLeave(123, "", "", "label", userID, sessionID); err != nil {
		return "", err
	}

	return "Success", nil
}

// Register as RPC function, this call should be in InitModule.
if err := initializer.RegisterRpc("leave", LeaveStream); err != nil {
	logger.Error("Unable to register: %v", err)
	return err
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let leaveFunction: nkruntime.RpcFunction = function(ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string) {
		let streamId: nkruntime.Stream = {
				mode: 123,
				label: 'my custom stream',
		};
		nk.streamUserLeave(ctx.userId, ctx.sessionId, streamId);
}

// Register as RPC function, this call should be in InitModule.
initializer.registerRpc('leave', leaveFunction);

사용자와 세션이 스트림의 구성원이 아닌 경우, 작동 상태는 no-op입니다.

채팅 채널실시간 멀티플레이어 대결과 같이, 클라이언트와 연결이 해제되면 일부로 구성된 모든 스트림에서 자동으로 제거됩니다.

스트림으로 데이터 전송 #

서버는 함수 호출을 통해 스트림으로 데이터를 전송할 수 있습니다. 현재 스트림에 있는 모든 사용자에게 메시지가 전달됩니다. 스트림으로 전송되는 메시지 페이로드는 모든 문자열을 사용할 수 있지만, JSON과 같이 구조화된 형식이 좋습니다.

Server
1
2
3
local stream_id = { mode = 123, label = "my custom stream" }
local payload = nk.json_encode({ some = "data" })
nk.stream_send(stream_id, payload)
Server
1
2
3
4
5
mode := uint8(123)
label := "label"
// Data does not have to be JSON, but it's a convenient format.
data := "{"some":"data"}"
nk.StreamSend(mode, "", "", label, data, nil)
Server
1
2
3
4
5
6
let streamId: nkruntime.Stream = {
	mode: 123,
	label: 'my custom stream',
};
const payload = {"some": "data"};
nk.streamSend(streamId, payload);

스트림이 비어 있는 경우, 작동 상태는 no-op입니다.

스트림 닫기 #

스트림을 닫으면 스트림에 있는 모든 현재 상태가 제거됩니다. 스트림을 명시적으로 닫고 서버가 더 신속하게 리소스를 회수하도록 활성화할 때 유용합니다.

Server
1
2
local stream_id = { mode = 123, label = "my custom stream" }
nk.stream_close(stream_id)
Server
1
2
3
mode := uint8(123)
label := "label"
nk.StreamClose(mode, "", "", label)
Server
1
2
3
4
5
let streamId: nkruntime.Stream = {
		mode: 123,
		label: 'my custom stream',
};
nk.streamClose(streamId);

스트림 현재 상태 계산 #

서버는 스트림 현재 상태의 전체 목록을 처리하지 않고도 현재 상태를 신속하게 계산할 수 있도록 현재 상태를 미리볼 수 있습니다.

Server
1
2
local stream_id = { mode = 123, label = "my custom stream" }
local count = nk.stream_count(stream_id)
Server
1
2
3
4
5
6
mode := uint8(123)
label := "label"
count, err := nk.StreamCount(mode, "", "", label)
if err != nil {
	// Handle error here.
}
Server
1
2
3
4
5
let streamId: nkruntime.Stream = {
		mode: 123,
		label: 'my custom stream',
};
let count = nk.streamCount(streamId);

스트림 현재 상태 나열 #

스트림 현재 상태 목록은 사용자가 연결된 세션 ID에 대한 정보와 추가적인 메타데이터를 포함하여 현재 온라인 상태이고 스트림에 연결된 모든 사용자를 포함합니다.

Server
1
2
3
4
5
6
local stream_id = { mode = 123, label = "my custom stream" }
local presences = nk.stream_user_list(stream_id)

for _, presence in ipairs(presences) do
	nk.logger_info("Found user ID " .. presence.user_id)
end
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
mode := uint8(123)
label := "label"
includeHidden := true
includeNotHidden := true

members, err := nk.StreamUserList(mode, "", "", label, includeHidden, includeNotHidden)
if err != nil {
	// Handle error here
}

for _, m := range members {
	logger.Info("Found user: %s\n", m.GetUserId())
}
Server
1
2
3
4
5
6
7
8
let streamId: nkruntime.Stream = {
		mode: 123,
		label: 'my custom stream',
};
let presences = nk.streamUserList(streamId);
presences?.forEach(function (p) {
		logger.info('Found user: %s\n', streamId);
});

스트림 현재 상태 확인 #

한 명의 사용자만 필요한 경우, 서버는 사용자가 현재 스트림에 있는지 확인하고 현재 상태 및 메타데이터를 회수할 수 있습니다.

예를 들어, 사용자 지정 스트림에서 호출하는 사용자가 활성화되어 있는지 확인하는 RPC 함수를 등록할 수 있습니다.

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
local function check(context, _)
	local stream_id = { mode = 123, label = "my custom stream" }
	local meta = nk.stream_user_get(context.user_id, context.session_id, stream_id)

	-- Meta is nil if the user is not present on the stream.
	if (meta) then
		nk.logger_info("User found on stream!")
	end
end
nk.register_rpc(check, "check")
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func CheckStream(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
	userID, ok := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)
	if !ok {
		// If user ID is not found, RPC was called without a session token.
		return "", errors.New("Invalid context")
	}
	sessionID, ok := ctx.Value(runtime.RUNTIME_CTX_SESSION_ID).(string)
	if !ok {
		// If session ID is not found, RPC was not called over a connected socket.
		return "", errors.New("Invalid context")
	}

	mode := uint8(123)
	label := "label"

	if metaPresence, err := nk.StreamUserGet(mode, "", "", label, userID, sessionID); err != nil {
		// Handle error.
	} else if metaPresence != nil {
		logger.Info("User found on stream")
	} else {
		logger.Info("User not found on stream")
	}

	return "Success", nil
}

// Register as RPC function, this call should be in InitModule.
if err := initializer.RegisterRpc("check", CheckStream); err != nil {
	logger.Error("Unable to register: %v", err)
	return err
}
Server
1
2
3
4
5
6
7
8
let streamId: nkruntime.Stream = {
		mode: 123,
		label: 'my custom stream',
};
let meta = nk.streamUserGet(ctx.userId, ctx.sessionId, streamId);
if (meta) {
		logger.info('User found on stream');
}

내장 스트림 #

채팅 채널, 멀티플레이어, 알림과 같은 서버의 실시간 기능은 스트림 위에 구성되어 있습니다.

이러한 스트림 구조에 대한 이해를 통해 코드 런타임은 어떠한 기능도 변경할 수 있습니다.

스트림모드주체설명자레이블정보
알림0사용자 ID--연결된 사용자에 대한 인앱 알림 전달을 제어합니다.
상태1사용자 ID--상태 기능 및 친구에게 업데이트를 전달하는 기능을 제어합니다.
채팅 채널2--“채널 이름”채팅 채널에 대한 구성원 자격.
그룹 채팅3그룹 ID--그룹의 개인적인 채팅 채널.
다이렉트 메시지4사용자 ID사용자 ID-두 사용자 간의 개인적인 다이렉트 메시지 대화.
중계 대결5대결 ID--중계된 실시간 멀티플레이어 대결에 대한 구성원 자격 및 메시지 라우팅.
권한 보유 대결6대결 ID-“nakama 노드 이름”권한 부여 실시간 멀티플레이어 대결에 대한 구성원 자격 및 메시지 라우팅.

위에서 설명한 함수에 스트림 식별자를 사용하면 해당 기능에서 내부적인 행위를 완전히 제어할 수 있습니다.

예: 채팅 채널에서 사용자 추방 #

이 코드를 사용하면 채팅 채널에서 사용자를 제거합니다. 사용자가 채널에 연결된 세션이 한 개 이상 있는 경우, 지정된 세션만 제거됩니다.

Server
1
2
3
4
local stream_id = { mode = 2, label = "some chat channel name" }
local user_id = "user ID to kick"
local session_id = "session ID to kick"
nk.stream_user_leave(user_id, session_id, stream_id)
Server
1
2
3
4
5
6
7
8
mode := uint8(123)
label := "some chat room channel name"
userID := "user ID to kick"
sessionID := "session ID to kick"

if err := nk.StreamUserLeave(mode, "", "", label, userID, sessionID); err != nil {
	// Handle error.
}
Server
1
2
3
4
5
let streamId: nkruntime.Stream = {
		mode: 123,
		label: 'my custom stream',
};
nk.streamUserLeave(ctx.userId, ctx.sessionId, streamId);

예: 알림 수신 정지 #

이 RPC 함수를 호출하여 사용자는 알림을 “무음"으로 설정할 수 있습니다. 사용자가 온라인 상태인 경우에도 인앱 알림이 실시간으로 전달되지 않습니다.

Server
1
2
3
4
5
local function enable_silent_mode(context, _)
	local stream_id = { mode = 0, subject = context.user_id }
	nk.stream_user_leave(context.user_id, context.session_id, stream_id)
end
nk.register_rpc(enable_silent_mode, "enable_silent_mode")
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func EnableSilentMode(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
	userID, ok := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)
	if !ok {
		// If user ID is not found, RPC was called without a session token.
		return "", errors.New("Invalid context")
	}
	sessionID, ok := ctx.Value(runtime.RUNTIME_CTX_SESSION_ID).(string)
	if !ok {
		// If session ID is not found, RPC was not called over a connected socket.
		return "", errors.New("Invalid context")
	}

	if err := nk.StreamUserLeave(0, userId, "", "", userID, sessionID); err != nil {
		// Handle error.
	}

	return "Success", nil
}

// Register as RPC function, this call should be in InitModule.
if err := initializer.RegisterRpc("enable_silent_mode", EnableSilentMode); err != nil {
	logger.Error("Unable to register: %v", err)
	return err
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let enableSilentModeFn: nkruntime.RpcFunction = function(ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string) {
		let streamId: nkruntime.Stream = {
				mode: 123,
				label: 'my custom stream',
		};
		nk.streamUserLeave(ctx.userId, ctx.sessionId, streamId);
}

// Register as RPC function, this call should be in InitModule.
initializer.registerRpc('enable_silent_mode', enableSilentModeFn);