Issue with dialing REGISTERED (but offline) users - voip

i'm facing the following scenario:
we have to local (REGISTERED) users (iOS apps pjSIP) which initiating local calls between each other.
the problem arise when one of the users (let's say user B) is closing the application few minutes after he successfully REGISTERS.
now, when user A tries to call user B we see that the INVITE is sent but we got no reply (e.g 180 ringing) from User B.
Note: when we are sending an invite to User B he get's Push notification to his device what cases him to open the app (and to Re-REGISTER)
our targets are:
1. determinate if user B (e.g the callee) is reachable before we are sending an INVITE in cases User B App is closed and his extension is still REGISTERED
2. be able to send invite to user B right after he REGISTER
we tried to solve this issue from many directions:
1.Qualify - tried to decrease the qualify time of the registersion period so user B will be UNAVAILABLE as soon as possible (and we will check the device state before we will dial) but it may cause to massive OPTIONS on our network, and it's not going to solve target #2
2.AMI Service - it can catch events like: User A Dials user B , User B is Ringing (180 Ringing) and save those statuses to ASTDB . all this logic will be prefomed before we will launch the dial.
this solution is clumsy and to cmplicated and it requires to watch yet another service
after some research i'v got to the conclusion that the most suitable solution will be to store the time of the last OPTIONS reply of each extension(requires a patch in chan_sip.c) . re-trigger sip options qualify request to user B before User A Dials . if the original value (e.g before we re-triggered the OPTIONS) is equal the value after we trigged the OPTIONS it means that User B has not replied OPTIONS.
i'm attaching the changes i'v preformed to complete this task.
i would like to know if solution for the issue is suitable and valid and of course if there is a better way to preform it.
These is the change in chan_sip (using asterisk 11.7)
i'v prefomed changes only on the following lines:
23492 to 23500
23485 /*! \brief Handle qualification responses (OPTIONS) */
23486 static void handle_response_peerpoke(struct sip_pvt *p, int resp, struct sip_request *req)
23487 {
23488 struct sip_peer *peer = /* sip_ref_peer( */ p->relatedpeer /* , "bump refcount on p, as it is being used in this function(handle_response_peerpoke)")*/ ; /* hope this is already refcounted! */
23489 int statechanged, is_reachable, was_reachable;
23490 int pingtime = ast_tvdiff_ms(ast_tvnow(), peer->ps);
23491
23492 time_t result = time(NULL);
23493 result = (int)result;
This is how should implement dialplan with the patch:
Dial(SIP/${dest},10,Rgb(check_extension,s,1));
I'm sending the call to context before inviting ( with the "b" option)
context check_extension {
s => {
Set(IS_REACHABLE=0);
Verbose(KOLA/LastQualify/${DEST}); // Display User B initial quliafy
Set(INITIAL_QUALIFY=${DB(KOLA/LastQualify/${DEST})}); // Store it
for(loop=0;${loop}<60;dialLoop=${loop}+1) { // Loop untill the qualify will be changes
System(/usr/sbin/asterisk -rx "sip qualify peer ${DEST}");
Wait(2); // we need to wait a while for a response
Set(LAST_QUALIFY=${DB(KOLA/LastQualify/${DEST})}); // set the new qualify
if (${LAST_QUALIFY} > ${INITIAL_QUALIFY}) { // if the new qualify is newer, User B is reachable
Set(IS_REACHABLE=1);
break;
}
}
if (${IS_REACHABLE} = 0) {
Verbose(Peer is not reachable);
Hangup();
}
}
}

Best option for that is not use asterisk.
Use kamailio or opensips project, it can handle thousands of options packets.
Also you HAVE rewrite your app so when it closed it UNREGISTER, as that described in sip RFC.
To summarize: you are using buggy application, and triing do on asterisk thing it not designed to(large amount of users with options). So correct answer - use correct tools for this task.

Related

Using ZMQ_XPUB_MANUAL with zeromq.js

I am trying to implement a pub/sub broker with ZeroMQ where it is possible to restrict clients from subscribing to prefixes they are not allowed to subscribe to. I found a tutorial that tries to achieve a similar thing using the ZMQ_XPUB_MANUAL option. With zeromq.js it is possible to set this option:
import * as zmq from "zeromq";
// ...
const socket = new zmq.XPublisher({ manual: true });
After setting this option I am able to receive the subscription messages by calling .receive() on this socket:
const [msg] = await socket.receive();
But I have no Idea how to accept this subscription. Usally this is done by calling setSockOpt with ZMQ_SUBSCRIBE but I don't know how to do this with zeromq.js.
Is there a way to call setSockOpt with zeromq.js or is there another way to accept a subscription?
Edit
I tried user3666197's suggestion to call setSockOpt directly, but I am not sure how to do this. Rather than doing that, I took another look in the sources and found this: https://github.com/zeromq/zeromq.js/blob/master/src/native.ts#L617
It seems like setSockOpt is exposed to the TypeScript side as protected methods of the Socket class. To try this out, I created my own class that inherits XPublisher and exposed an acceptSubscription message:
class CustomPublisher extends zmq.XPublisher {
constructor(options?: zmq.SocketOptions<zmq.XPublisher>) {
super(options);
}
public acceptSubscription(subscription: string | null): void {
// ZMQ_SUBSCRIBE has a value of 6
// reference:
// https://github.com/zeromq/libzmq/blob/master/include/zmq.h#L310
this.setStringOption(6, subscription);
}
}
This works like a charm! But do not forget to strip the first byte of the subscription messages, otherwise your client won't receive any messages since the prefix won't match.
Q : "Is there a way to call setSockOpt() with zeromq.js or is there another way to accept a subscription?"
So, let me first mention Somdoron to be, out of doubts & for ages, a master of the ZeroMQ tooling.
Next comes the issue. The GitHub-sources, I was able to review atm, seem to me, that permit the ZMQ_XPUB-Socket-archetypes to process the native API ZMQ_XPUB_MANUAL settings ( re-dressed into manual-property, an idiomatic shift ), yet present no method (so far visible for me) to actually permit user to meet the native API explicit protocol of:
ZMQ_XPUB_MANUAL: change the subscription handling to manual...with manual mode subscription requests are not added to the subscription list. To add subscription the user need to call setsockopt() with ZMQ_SUBSCRIBE on XPUB socket./__ from ZeroMQ native API v.4.3.2 documentation __/
Trying to blind-call the Socket-inherited .SetSockOpt() method may prove me wrong, yet if successful, it may be a way to inject the { ZMQ_SUBSCRIBE | ZMQ_UNSUBSCRIBE } subscription-management steps into the XPUB-instance currently having been switched into the ZMQ_XPUB_MANUAL-mode.
Please test it, and if it fails to work via this super-class inherited method, the shortest remedy would be to claim that collision/conceptual-shortcomings directly to the zeromq.js maintainers ( it might be a W.I.P. item, deeper in their actual v6+ refactoring backlog, so my fingers are crossed for either case ).

Lync 2013 SDK - Join Conference & Connect AVModality when "Join meeting audio from" setting set to "Do not join audio"

I'm rather new to the Lync 2013 SDK (been using it for a couple weeks now) and have been able to figure out mostly everything I need except for this...
When I'm joining a conference (using ConversationManager.JoinConference()) it joins fine. However, in certain cases (not all), I want to then connect the AVModality on the conference. Sometimes it works, sometimes it just sits in "Connecting" and never connects (even though I've called EndConnect).
What I've found is the setting in Skype's Options -> Skype Meetings -> Joining conference calls section, seems to override my code. Maybe a race condition?
When the setting is "Do not join audio" and "Before I join meetings, ask me which audio device I want to use" is NOT CHECKED (meaning I get no prompt when joining): the conference joins, the AVModality goes Disconnected -> Connecting -> Disconnected. Then my code triggers a BeginConnect and the AVModality goes Disconnected -> Connecting - and never resolves (sometimes I get a fast busy tone audio sound).
When the "Before I join meetings, ask me which audio device I want to use" IS CHECKED (meaning I get the prompt): the conference joins, the prompt asks how to connect, if I select Skype for business - it connects audio fine (expected). Interestingly, if I hang up the call using the Lync UI (AVModality goes to Disconnected), it then immediately connects back again (assuming my BeginConnect doing this).
Here's where it gets really convoluted:
If I call BeginConnect when the state is Connecting on the AVmodality within the ModalityStateChanged event handler... the following happens:
Conference joins, prompt asks me how to connect (AVmodality state is "Connecting" at this point until a decision is made on the prompt) - this means my BeginConnect fires. Then if I choose "Do not join audio" in the prompt... the AVModality status goes Connecting -> Disconnected -> Connecting -> Joining -> Connected. So - my BeginConnect is already in progress and still works in this case so long as it fires BEFORE the selection of "Do not join audio".
So I'm wondering if the "Do not join audio" selection (whether with or without the prompt) actually sets some other properties on something which prevents the AVModality from being connected after that point without doing some additional hocus pocus? If so - I'd like to know the additional hocus pocus I need to perform :)
Thanks for any and all help!
It's come down to this... whether the conference joining does join the audio or not - I've handled every scenario except one, which I still can't figure out:
1. I need the conference audio to be joined, but the user selects to NOT join the audio (either on the prompt, or from the Skype options settings).
In this case - I have added an event handler to the modality state change event, and when the NewState == Disconnected, I trigger a BeginConnect on the modality itself. This works fine. Within the callback, I have the EndConnect call. However - the AVModality state continues to stay in "Connecting" and never resolves to being connected. On the UI - it shows the audio buttons, but all grayed out (like normal when it's connecting). I'm not sure how to make it finish connecting?
Here's a snippet of code:
if (merge)
{
myHandler = delegate (object sender1, ModalityStateChangedEventArgs e1)
{
AVModality avModality = (AVModality)sender1;
Globals.ThisAddIn.confConvo = avModality.Conversation;
if (e1.NewState == ModalityState.Connected)
{
DialNumberInSkype(meetingInfo);
avModality.ModalityStateChanged -= myHandler;
}
if (e1.NewState == ModalityState.Disconnected)
{
object[] asyncState = { avModality, "CONNECT" };
avModality.BeginConnect((ar) =>
{
avModality.EndConnect(ar);
DialNumberInSkype(meetingInfo);
}, asyncState);
avModality.ModalityStateChanged -= myHandler;
}
};
}
EDIT:
For some reason, I'm not able to add a comment right now...
I tried setting the endpoint as you suggested. However, I get an ArgumentException error "Value does not fall within the expected range." So I tried hardcoding the uri value in the CreateContactEndpoint to "sip:my_login#domain.com" (except real value of course) - and got the same ArgumentException error. I added a breakpoint before this and was able to see the value for the avModality.Endpoint - and it is actually set to me the entire time... it's not null or unset when I'm trying to call BeginConnect.
When JoinConference() is invoked audio modality will be connected even without explicitly invoking BeginConnect().
When prompt asking for audio device selection is shown(when ask before join option is set in skype) conversation property ConferenceEscalationProgress will be having value AwaitingJoinDialogResponse.
Setting conversation property ConferenceJoinDialogCompleted as true will initiate Modality connection even though the prompt is not closed.
Edited
If do not join audio is selected, modality will be disconnected, at this point you are trying to invoke BeginConnect(). Try setting modality endpoint before invoking BeginConnect().
conversation.Modalities[ModalityTypes.AudioVideo].Endpoint = lyncClient.Self.Contact.CreateContactEndpoint(lyncClient.Self.Contact.Uri);

Firebase onDisconnect() firing multiple times

Building an app with presence following the firebase docs, is there a scenario where the on-disconnect fires when the app is still connected? We see instances where the presence node shows the app as going offline and then back online within a few seconds when we aren't losing a network connection.
We are seeing on multiple embedded devices installed in the field where presence is set to false and then almost immediately right back to true and it's occurring on all the devices within a few seconds of each other. From the testing we have done and the docs online we know that if we lose internet connection on the device it takes roughly 60 seconds before the timeout on the server fires the onDisconnect() method.
We have since added code in the presence method that allows the device if it sees the presence node be set to false while the app is actually running it will reset the presence back to true. At times when this happens we get a single write back to true and that is the end of it, other times it is like the server and client are fighting each other and the node is reset to true numerous times over the course of 50-200 milliseconds. We monitor this by pushing to another node within the device GUID each time we are forcing presence back to true. This only occurs while the module is running and after it initially establishes presence.
Here is the method that we call from our various modules that are running on the device so that we can monitor the status of each of the modules at any given time.
exports.online = function (program, currentProgram) {
var programPath = process.env.FIREBASE_DEVICES + process.env.GUID + '/status/' + program
var onlinePath = process.env.FIREBASE_DEVICES + process.env.GUID + '/statusOnlineTimes/' + program
var programRef = new firebase(programPath);
var statusRef = new firebase(process.env.FIREBASE_DEVICES + process.env.GUID + '/status/bootup');
var onlineRef = new firebase(onlinePath)
amOnline.on('value', function(snapshot) {
if (snapshot.val()) {
programRef.onDisconnect().set(false);
programRef.set(true);
programRef.on('value', function(snapshot){
if (snapshot.val() == false){
programRef.set(true);
console.log('[NOTICE] Resetting', program, 'module status back to True after Fireabase set to False')
var objectToPush = {
program: program,
time: new Date().toJSON()
}
onlineRef.push(objectToPush)
}
})
if (currentProgram != undefined) {
statusRef.onDisconnect().set('Offline')
statusRef.set(currentProgram)
}
}
});
The question we have is there ever an instance where Firebase is calling the onDisconnect() method even though it really isn't losing its status? We had instances where we would see the device go offline and then back online within 60 seconds before we added the reset code. The reset code was to combat another issue we had in the field where if the power were interrupted to the device and it did not make a clean exit, the device could reboot and and reset the presence with a new UID before the timeout for the prior instance had fired. Then once the timeout fired the device would show as offline even though it was actually online.
So we were able to stop the multiple pushes that were happening when the device reconnected by adding a programRef.off() call directly before the programRef.on(...) call. What we determined to be happening is that anytime the device went online from an offline state and the amOnline.on(...) callback fired it created a new listener.
Now we are able to handle the case where a onDisconnect() fires from a earlier program PID and overwrites the currently active program with a status of offline. This seems to solve the issue we are having with the race condition of the devices in the field able to reboot and regain connection prior to the onDisconnect() firing for the instance that was not cleanly exited.
We are still having an issue where all of the devices are going off and then back online at approximately the same time (within 1-3 seconds of each other). Are there any times where Firebase resets the ./info/connected node? Because we are monitoring presence and actually logging on and off events maybe we are just catching an event that most people don't see? Or is there something that we are doing wrong?

What is the proper way to do GameCenter authentication?

I have seen in posts around stack overflow that shows snippets of handling GameCenter authentication. However, none of these solutions address any of the problems that real world use cases cover. Namely, the [GKLocalPlayer localPlayer].authenticateHandler is just a call back of the status, and not much else. It provides a view controller, but there are massive inconsistencies in .authenticated, and error states.
There are a few things I am trying to do:
1. Not pop up the game center login until a feature uses it
2. Try to authenticate silently on app launch
3. Provide some info to the user why GameCenter features are not working
4. Provide a recovery mechanism
Namely if there is an error reported how can I show the login dialog anyways?
I get this error with no viewController:
Case 1:
Error in GameCenterManager::authenticateLocalPlayer [The Internet connection appears to be offline.]
Despite its error message, the device is completely online, as safari loads cnn.com just fine.
Case 2:
Someone closes the login screen because they are not ready, in which case .authenticated comes back as true, viewController remains at nil, yet all game center calls will fail. Why is the [GKLocalPlayer localPlayer].authenticated set to true when it isn't?
Case 3:
Error in GameCenterManager::authenticateLocalPlayer [The operation
couldn’t be completed. (NSURLErrorDomain error -1009.)]
This keeps occurring yet there is nothing the app can do for the user. In this case what should the messaging be? Switch apps to Game Center and login there?
Case 4:
Error in GameCenterManager::authenticateLocalPlayer [The requested
operation has been canceled or disabled by the user.]
This happens if the user cancels the viewController the app was told to present by apple. Yet, there is also no recovery or detecting this state.
Case 5:
Error in GameCenterManager::createMatch [The requested operation could
not be completed because local player has not been authenticated.]
This happens if the user was logged in, but for whatever reason logs out of GameCenter then returns to the app. The app will be told the user is still authenticated when it is clearly not, yet there are no calls I can make to bring up another login.
So essentially, if GameCenter doesn't just silently work, what are we to do as app designers? Alert view and tell them to go login using the game center app and restart the app?
Here is my authentication code:
//******************************************************
// Authenticate
//******************************************************
-(void)authenticateLocalPlayer:(bool)showLogin
{
if( showLogin && self.loginScreen != nil )
{ [[WordlingsViewController instance] presentViewController:self.loginScreen animated:YES completion:nil]; }
if( [GKLocalPlayer localPlayer].isAuthenticated )
{
NSDLog(NSDLOG_GAME_CENTER,#"GameCenterManager::authenticateLocalPlayer LocalPlayer authenticated");
}
__weak GameCenterManager* weakSelf = self;
[GKLocalPlayer localPlayer].authenticateHandler = ^(UIViewController *viewController, NSError *error)
{
if (error != nil)
{
NSDLog(NSDLOG_GAME_CENTER,#"Error in GameCenterManager::authenticateLocalPlayer [%#]", [error localizedDescription]);
}
else
{
if (viewController != nil)
{
NSDLog(NSDLOG_GAME_CENTER,#"GameCenter: No authentication error, but we need to login");
weakSelf.loginScreen = viewController;
}
else
{
if ( [GKLocalPlayer localPlayer].authenticated )
{
NSDLog(NSDLOG_GAME_CENTER,#"GameCenter localPlayer authenticated");
weakSelf.gameCenterAvailable = YES;
weakSelf.localPlayer = [GKLocalPlayer localPlayer];
[weakSelf retrieveFriends];
[weakSelf loadPlayerPhoto:weakSelf.localPlayer];
for ( id<GameCenterDelegate> listener in weakSelf.listeners )
{ [listener onPlayerAuthenticated]; }
}
else
{
weakSelf.gameCenterAvailable = NO;
}
}
}
};
}
This function is called twice: once at app startup to hopefully create a valid login state, and 2nd if the user is not authenticated and they try to use an app feature that requires game center. In this app, it is creating a turn based match or viewing friends
You're encountering many of the same complaints I have about the Game Center API. I've been trying to achieve the same 4 things you are. The TL;DR version: Game Center simply doesn't support it. >< But there are some things you can do to reduce the pain.
One general thing that helped me: make sure to check both the NSError as well as it's .underlyingError property. I've seen several cases where the NSError is too vague to be helpful, but the underlying error has more specific details.
Case 1: Can you share the error domain and error code for the both the NSError and the underlyingError?
Case 2: I have an open bug with Apple on this, for a looooong time. There are several cases, including being in Airplane mode, where the authentication fails but .authenticated returns true. When I wrote a bug on this, Apple closed it saying this was "by design" so players could continue to play the game using any previously cached data. So, I appended the bug with several scenarios where cached data causes significant problems. My bug was re-opened and has sat there ever since. The design philosophy seems to be: "well, just keep going and maybe it will all work out in the end." But it doesn't work out in the end, the user gets stuck, unable to play and they blame my game, not Apple.
The only mitigation I have found is exactly what you're already doing: Always always always check the NSError first in the authentication handler. The view controller and .authenticated are totally unreliable if an error has been set.
If there is an error, I pass it to one dedicated error handler that displays alerts to users and tells them what they need to do to recover.
Case 3: I have hit -1009 as well. From what I can discern it happens when I have network connection, but Game Center never replied. That could come from any disruption anywhere between my router up-to-and-including Game Center servers not responding. I used to see this a lot when using the GC Test Servers. Not so much now that the test servers were merged into the prod environment.
Case 4: You are 100% correct. there is no in-game recovery. If the user cancels the authentication, that's the end of the line. The only way to recover is to kill the game (not just leave and re-enter) and restart it. Then, and only then, you can present another login view controller.
There are some things you can do to mitigate this, though. It will directly break your #1 goal of delaying login until needed, but I haven't found anything better:
Disable the "start game" button (or whatever you have in your game) until you've confirmed the login succeeded with no errors AND you can successfully download a sample leaderboard from GC. This proves end-to-end connectivity.
If the user cancels the login, your authentication handler will receive an NSError of domain = GKErrorDomain and code = GKErrorCanceled. WHen I see that combo, I put up a warning to the user that they cannot play network games until they've successfully logged in and to login they will have to stop and restart the game.
Users were confused why the "start" button was disabled, so I added an alert there too. I show a button that looks disabled but is really enabled. And when they try to click it, I again present an alert telling them they have to login in to game center to play a network game.
It sucks, but at least the user isn't stuck.
Case 5: This is one of the examples I cited in my bug referred to in case 2. By letting the user think they're logged in when they really aren't, they try to do things they really can't do, and eventually something bad will happen.
The best mitigation I have found for this is the same as Case 4: don't let the user start a session until you see the authentication handler fire with no errors AND you can successfully download a sample leaderboard to prove the network connection.
In fact, doing a search through all of my code bases, I never use .authenticated for any decisions anymore.
Having said all of that, here's my authentication handler. I won't say it's pretty, but thus far, users don't get stuck in unrecoverable situations. I guess it's a case of desperate times (working with a crap API) requires desperate measures (kludgy work arounds).
[localPlayer setAuthenticateHandler:^(UIViewController *loginViewController, NSError *error)
{
//this handler is called once when you call setAuthenticated, and again when the user completes the login screen (if necessary)
VLOGS (LOWLOG, SYMBOL_FUNC_START, #"setAuthenticateHandler completion handler");
//did we get an error? Could be the result of either the initial call, or the result of the login attempt
if (error)
{
//Here's a fun fact... even if you're in airplane mode and can't communicate to the server,
//when this call back fires with an error code, localPlayer.authenticated is set to YES despite the total failure. ><
//error.code == -1009 -> authenticated = YES
//error.code == 2 -> authenticated = NO
//error.code == 3 -> authenticated = YES
if ([GKLocalPlayer localPlayer].authenticated == YES)
{
//Game center blatantly lies!
VLOGS(LOWLOG, SYMBOL_ERROR, #"error.code = %ld but localPlayer.authenticated = %d", (long)error.code, [GKLocalPlayer localPlayer].authenticated);
}
//show the user an appropriate alert
[self processError:error file:__FILE__ func:__func__ line:__LINE__];
//disable the start button, if it's not already disabled
[[NSNotificationCenter defaultCenter] postNotificationName:EVENT_ENABLEBUTTONS_NONETWORK object:self ];
return;
}
//if we received a loginViewContoller, then the user needs to log in.
if (loginViewController)
{
//the user isn't logged in, so show the login screen.
[appDelegate presentViewController:loginViewController animated:NO completion:^
{
VLOGS(LOWLOG, SYMBOL_FUNC_START, #"presentViewController completion handler");
//was the login successful?
if ([GKLocalPlayer localPlayer].authenticated)
{
//Possibly. Can't trust .authenticated alone. Let's validate that the player actually has some meaningful data in it, instead.
NSString *alias = [GKLocalPlayer localPlayer].alias;
NSString *name = [GKLocalPlayer localPlayer].displayName;
if (alias && name)
{
//Load our matches from the server. If this succeeds, it will enable the network game button
[gameKitHelper loadMatches];
}
}
}];
}
//if there was not loginViewController and no error, then the user is already logged in
else
{
//the user is already logged in, so load matches and enable the network game button
[gameKitHelper loadMatches];
}
}];

Node.js w/ Socket.io - switch users function (allow and prevent writing)

I'm building something with node.js and socket.io which allows users to write in a textarea (pretty much like a tchat), but I need them to write alternately. Something like:
User 1 is writing. User 2 and User 3 can't write.
User 1 send the message.
User 1 can't write. User 2 is allowed to write. User 3 can't write.
User 2 send the message.
User 1 and User 2 can't write. User 3 is allowed to write.
User 3 send the message.
User 1 is writing. User 2 and User 3 can't write.
... etc
For now, I have (on the client side) :
var ucan;
$('#txtform').submit(function(event){
if(ucan){
socket.emit('trigger', me);
ucan = false;
}
$('#txtArea').attr('readonly','true');
}
})
on the server side :
socket.on('trigger', function(user){
u = user.id + 1; // switch to next user since users[] (further)
// stores all the users with their ids
if(u >= users.length){
u = 0; // loop throug all users
}
io.sockets.socket( users[u] ).emit('turn');
})
which makes me on the client side again :
socket.on('turn', function(){
ucan = true;
$('#txtArea').removeAttr('readonly');
})
The problems are that when they connect on the app, new users have the permission to write, so the first round they can all write at the same time, and when they all have written, permission does not loop and nobody can write.
I thought maybe something exists inside node.js or socket.io which allows me to do this more simply (the way I did is probably not the best), or anything else, but since I'm a beginner and I found nothing on the web, I'm asking for your help.
Thank you !
p.s: please excuse my english it is not my first language :)
Have it readonly by default, then on the server side when a user connects, if they are the only socket connected, emit the 'turn'. You should probably also handle the case where the person whose turn it is disconnects, at which point you should do your trigger and let someone else have control.

Resources