5180 fichiers 38.5 Mo 0 téléchargements 221 vues mis à jour il y a 5 heures ui
nitro-v36-with-renderer-v36 / src / hooks / groups / useGroupMemberRemoval.ts
useGroupMemberRemoval.ts 5 Ko Télécharger le fichier
import { GetCommunication, GetSessionDataManager, GroupConfirmMemberRemoveEvent, GroupConfirmRemoveMemberComposer, OctaneEventType } from '@octane/renderer';
import { useCallback, useEffect, useState } from 'react';
import { SendMessageComposer } from '../../api';
import { useMessageEvent, useOctaneEvent } from '../events';

/**
 * GroupConfirmMemberRemove is answered with only a userId and a furniture count: the reply names neither the group nor the request
 * it belongs to, and the server may not answer at all (the live emulator stays silent for an ordinary member's own leave request).
 * No timing or ordering rule can bind such a reply safely, so the group windows share one outstanding transaction:
 * - while it is unresolved no other consumer may send GroupConfirmRemoveMember;
 * - the first reply for its user resolves it, and only for the window that owns it; the reply of a closed window is retired by the sink;
 * - a confirmation captured from a reply is valid only for the group on screen and the signed-in user it was captured for (isCurrentSession);
 * - there is no timeout, a late reply is never rebound to a later request;
 * - an unanswered request therefore blocks further requests until the session user changes or the connection is lost: the sink drops the
 *   transaction when the connection is no longer authenticated, so a request sent on the old socket can neither block the new one nor be
 *   answered for it, and a confirmation shown before the loss is no longer valid (isCurrentSession compares the connection epoch).
 */
interface RemovalTransaction {
    owner: number;
    groupId: number;
    userId: number;
    sessionUserId: number;
    epoch: number;
}

let transaction: RemovalTransaction = null;
let nextOwner = 1;
// Bumped every time the connection is lost.
let epoch = 0;
const liveOwners = new Set<number>();

const currentSessionUserId = () => GetSessionDataManager().userId;

const dropForeignSession = () =>
{
    if (transaction && transaction.sessionUserId !== currentSessionUserId()) transaction = null;
};

/** The outstanding transaction when the reply is for it and `owner` is the window that asked. */
const claimOwned = (owner: number, replyUserId: number): RemovalTransaction =>
{
    dropForeignSession();

    const request = transaction;

    if (!request || request.owner !== owner || request.userId !== replyUserId) return null;

    transaction = null;

    return request;
};

/** Retires the reply for a transaction whose window is gone. The request is discarded, never handed to another window. */
const retireOrphaned = (replyUserId: number) =>
{
    dropForeignSession();

    if (transaction && transaction.userId === replyUserId && !liveOwners.has(transaction.owner)) transaction = null;
};

export interface GroupMemberRemovalActions {
    /** Asks the server what removing userId would cost; false when another request is still unresolved (nothing is sent). */
    request: (groupId: number, userId: number) => boolean;
    /** The transaction this consumer owns when the reply for userId arrives, otherwise null. */
    claimReply: (userId: number) => GroupMemberRemoval;
    /** Whether a confirmation captured from claimReply still belongs to the signed-in user (a dialog can outlive a login). */
    isCurrentSession: (removal: GroupMemberRemoval) => boolean;
}

export interface GroupMemberRemoval {
    groupId: number;
    userId: number;
    sessionUserId: number;
    epoch: number;
}

export const useGroupMemberRemoval = (): GroupMemberRemovalActions =>
{
    // One id per window for as long as it is mounted.
    const [owner] = useState<number>(() => nextOwner++);

    useEffect(() =>
    {
        liveOwners.add(owner);

        return () =>
        {
            liveOwners.delete(owner);
        };
    }, [owner]);

    const request = useCallback((groupId: number, userId: number) =>
    {
        dropForeignSession();

        if (transaction) return false;

        transaction = { owner, groupId, userId, sessionUserId: currentSessionUserId(), epoch };
        SendMessageComposer(new GroupConfirmRemoveMemberComposer(groupId, userId));

        return true;
    }, [owner]);

    const claimReply = useCallback((userId: number) => claimOwned(owner, userId), [owner]);
    const isCurrentSession = useCallback((removal: GroupMemberRemoval) => removal.sessionUserId === currentSessionUserId() && removal.epoch === epoch, []);

    return { request, claimReply, isCurrentSession };
};

/** Mounted once for the whole client: resolves a reply whose owning window has closed, so it can neither block nor leak to another window. */
export const useGroupMemberRemovalSink = () =>
{
    // The server forgets the request with the socket: nothing is answered for it on the next connection.
    useOctaneEvent(OctaneEventType.CONNECTION_STATE_CHANGED, () =>
    {
        if (GetCommunication().connection.connectionState.authenticated) return;

        epoch++;
        transaction = null;
    });

    useMessageEvent<GroupConfirmMemberRemoveEvent>(GroupConfirmMemberRemoveEvent, (event) =>
    {
        retireOrphaned(event.getParser().userId);
    });
};
15Sujets
1Dépôts
20Membres
2En ligne
Profile
Inscrit
Dernière visite
Messages
0
Votes positifs
0
Votes négatifs
0

Nous utilisons des cookies pour assurer le bon fonctionnement du site et améliorer votre expérience. en savoir plus

Autoriser