Added basic invite link functionality
parent
7a00f2471d
commit
41876e273c
|
@ -0,0 +1,34 @@
|
|||
import {MessageCommandErrorResult} from "./Messages";
|
||||
import {clientServiceLogger} from "./Logging";
|
||||
|
||||
export type ActionResult<T> = {
|
||||
unwrap() : T;
|
||||
} & ({
|
||||
status: "success",
|
||||
result: T
|
||||
} | {
|
||||
status: "error",
|
||||
result: MessageCommandErrorResult
|
||||
});
|
||||
|
||||
|
||||
export function createErrorResult<T>(result: MessageCommandErrorResult) : ActionResult<T> {
|
||||
return {
|
||||
status: "error",
|
||||
result: result,
|
||||
unwrap(): T {
|
||||
clientServiceLogger.logError("Tried to unwrap an action which failed: %o", result);
|
||||
throw "action failed with " + result.type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createResult<T>(result: T) : ActionResult<T> {
|
||||
return {
|
||||
status: "success",
|
||||
result: result,
|
||||
unwrap(): T {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ import {
|
|||
import {geoLocationProvider} from "./GeoLocation";
|
||||
import {clientServiceLogger} from "./Logging";
|
||||
import {ClientServiceConnection} from "./Connection";
|
||||
import {Registry} from "tc-events";
|
||||
|
||||
export type LocalAgent = {
|
||||
clientVersion: string,
|
||||
|
@ -25,9 +26,17 @@ export interface ClientServiceConfig {
|
|||
generateHostInfo() : LocalAgent;
|
||||
}
|
||||
|
||||
export interface ClientServiceEvents {
|
||||
/** Client service session has successfully be initialized */
|
||||
notify_session_initialized: {},
|
||||
/** The current active client service session has been closed */
|
||||
notify_session_closed: {}
|
||||
}
|
||||
|
||||
export class ClientServices {
|
||||
readonly config: ClientServiceConfig;
|
||||
private connection: ClientServiceConnection;
|
||||
readonly events: Registry<ClientServiceEvents>;
|
||||
private readonly connection: ClientServiceConnection;
|
||||
|
||||
private sessionInitialized: boolean;
|
||||
private retryTimer: any;
|
||||
|
@ -36,6 +45,7 @@ export class ClientServices {
|
|||
private initializeLocaleId: number;
|
||||
|
||||
constructor(config: ClientServiceConfig) {
|
||||
this.events = new Registry<ClientServiceEvents>();
|
||||
this.config = config;
|
||||
this.initializeAgentId = 0;
|
||||
this.initializeLocaleId = 0;
|
||||
|
@ -44,6 +54,9 @@ export class ClientServices {
|
|||
this.connection = new ClientServiceConnection(5000);
|
||||
this.connection.events.on("notify_state_changed", event => {
|
||||
if(event.newState !== "connected") {
|
||||
if(this.sessionInitialized) {
|
||||
this.events.fire("notify_session_closed");
|
||||
}
|
||||
this.sessionInitialized = false;
|
||||
return;
|
||||
}
|
||||
|
@ -60,20 +73,30 @@ export class ClientServices {
|
|||
return;
|
||||
}
|
||||
|
||||
this.sendInitializeAgent().then(undefined);
|
||||
this.sendInitializeAgent().then(status => {
|
||||
switch (status) {
|
||||
case "aborted":
|
||||
return;
|
||||
|
||||
case "error":
|
||||
clientServiceLogger.logError("Failed to initialize session. Closing it and trying again in 60 seconds.");
|
||||
this.scheduleRetry(60 * 1000);
|
||||
return;
|
||||
|
||||
case "success":
|
||||
this.sessionInitialized = true;
|
||||
this.events.fire("notify_session_initialized");
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
/* The locale does not really matter for the session so just run it async */
|
||||
this.sendLocaleUpdate().then(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
this.connection.events.on("notify_notify_received", event => {
|
||||
switch (event.notify.type) {
|
||||
case "NotifyClientsOnline":
|
||||
this.handleNotifyClientsOnline(event.notify.payload);
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
this.connection.registerNotifyHandler("NotifyClientsOnline", notify => {
|
||||
clientServiceLogger.logInfo("Received user count update: %o", notify);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -89,6 +112,10 @@ export class ClientServices {
|
|||
this.initializeLocaleId++;
|
||||
}
|
||||
|
||||
getConnection() : ClientServiceConnection {
|
||||
return this.connection;
|
||||
}
|
||||
|
||||
private scheduleRetry(time: number) {
|
||||
this.stop();
|
||||
|
||||
|
@ -97,12 +124,13 @@ export class ClientServices {
|
|||
|
||||
/**
|
||||
* Returns as soon the result indicates that something else went wrong rather than transmitting.
|
||||
* Note: This will not throw an exception!
|
||||
* @param command
|
||||
* @param retryInterval
|
||||
*/
|
||||
private async executeCommandWithRetry(command: MessageCommand, retryInterval: number) : Promise<MessageCommandResult> {
|
||||
while(true) {
|
||||
const result = await this.connection.executeCommand(command);
|
||||
const result = await this.connection.executeMessageCommand(command);
|
||||
switch (result.type) {
|
||||
case "ServerInternalError":
|
||||
case "CommandEnqueueError":
|
||||
|
@ -133,7 +161,10 @@ export class ClientServices {
|
|||
}
|
||||
}
|
||||
|
||||
private async sendInitializeAgent() {
|
||||
/**
|
||||
* @returns `true` if the session agent has been successfully initialized.
|
||||
*/
|
||||
private async sendInitializeAgent() : Promise<"success" | "aborted" | "error"> {
|
||||
const taskId = ++this.initializeAgentId;
|
||||
|
||||
const hostInfo = this.config.generateHostInfo();
|
||||
|
@ -148,12 +179,22 @@ export class ClientServices {
|
|||
|
||||
if(this.initializeAgentId !== taskId) {
|
||||
/* We don't want to send that stuff any more */
|
||||
return;
|
||||
return "aborted";
|
||||
}
|
||||
|
||||
this.executeCommandWithRetry({ type: "SessionInitializeAgent", payload }, 2500).then(result => {
|
||||
const result = await this.executeCommandWithRetry({ type: "SessionInitializeAgent", payload }, 2500);
|
||||
if(this.initializeAgentId !== taskId) {
|
||||
/* We don't want to send that stuff any more */
|
||||
return "aborted";
|
||||
}
|
||||
|
||||
if(result.type === "Success") {
|
||||
clientServiceLogger.logTrace("Agent initialized", result);
|
||||
return "success";
|
||||
} else {
|
||||
clientServiceLogger.logTrace("Agent initialize result: %o", result);
|
||||
});
|
||||
return "error";
|
||||
}
|
||||
}
|
||||
|
||||
private async sendLocaleUpdate() {
|
||||
|
@ -173,12 +214,11 @@ export class ClientServices {
|
|||
return;
|
||||
}
|
||||
|
||||
this.connection.executeCommand({ type: "SessionUpdateLocale", payload }).then(result => {
|
||||
clientServiceLogger.logTrace("Agent local update result: %o", result);
|
||||
});
|
||||
}
|
||||
const result = await this.connection.executeCommand("SessionUpdateLocale", payload);
|
||||
if(this.initializeLocaleId !== taskId) {
|
||||
return;
|
||||
}
|
||||
|
||||
private handleNotifyClientsOnline(notify: NotifyClientsOnline) {
|
||||
clientServiceLogger.logInfo("Received user count update: %o", notify);
|
||||
clientServiceLogger.logTrace("Agent local update result: %o", result);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
import {ClientServices} from "./ClientService";
|
||||
import {ActionResult, createErrorResult, createResult} from "./Action";
|
||||
|
||||
export type InviteLinkInfo = {
|
||||
linkId: string,
|
||||
|
||||
timestampCreated: number,
|
||||
timestampDeleted: number,
|
||||
|
||||
|
||||
amountViewed: number,
|
||||
amountClicked: number,
|
||||
|
||||
propertiesConnect: {[key: string]: string},
|
||||
propertiesInfo: {[key: string]: string},
|
||||
};
|
||||
export class ClientServiceInvite {
|
||||
private readonly handle: ClientServices;
|
||||
|
||||
constructor(handle: ClientServices) {
|
||||
this.handle = handle;
|
||||
}
|
||||
|
||||
async createInviteLink(connectProperties: {[key: string]: string}, infoProperties: {[key: string]: string}, createNew: boolean) : Promise<ActionResult<{ linkId: string, adminToken: string }>> {
|
||||
const connection = this.handle.getConnection();
|
||||
|
||||
const notify = connection.catchNotify("NotifyInviteCreated");
|
||||
const result = await connection.executeCommand("InviteCreate", {
|
||||
new_link: createNew,
|
||||
properties_connect: connectProperties,
|
||||
properties_info: infoProperties
|
||||
});
|
||||
const notifyResult = notify();
|
||||
|
||||
if(result.type !== "Success") {
|
||||
return createErrorResult(result);
|
||||
}
|
||||
|
||||
if(notifyResult.status === "fail") {
|
||||
return createErrorResult({ type: "GenericError", error: "failed to receive notify" });
|
||||
}
|
||||
|
||||
return createResult({
|
||||
adminToken: notifyResult.value.admin_token,
|
||||
linkId: notifyResult.value.link_id
|
||||
});
|
||||
}
|
||||
|
||||
async queryInviteLink(linkId: string, registerView: boolean) : Promise<ActionResult<InviteLinkInfo>> {
|
||||
const connection = this.handle.getConnection();
|
||||
|
||||
const notify = connection.catchNotify("NotifyInviteInfo", notify => notify.link_id === linkId);
|
||||
const result = await connection.executeCommand("InviteQueryInfo", {
|
||||
link_id: linkId,
|
||||
register_view: registerView
|
||||
});
|
||||
const notifyResult = notify();
|
||||
|
||||
if(result.type !== "Success") {
|
||||
return createErrorResult(result);
|
||||
}
|
||||
|
||||
if(notifyResult.status === "fail") {
|
||||
return createErrorResult({ type: "GenericError", error: "failed to receive notify" });
|
||||
}
|
||||
|
||||
return createResult({
|
||||
linkId: notifyResult.value.link_id,
|
||||
|
||||
amountClicked: notifyResult.value.amount_clicked,
|
||||
amountViewed: notifyResult.value.amount_viewed,
|
||||
|
||||
timestampCreated: notifyResult.value.timestamp_created,
|
||||
timestampDeleted: notifyResult.value.timestamp_deleted,
|
||||
|
||||
propertiesConnect: notifyResult.value.properties_connect,
|
||||
propertiesInfo: notifyResult.value.properties_info,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -12,9 +12,11 @@ type PendingCommand = {
|
|||
|
||||
interface ClientServiceConnectionEvents {
|
||||
notify_state_changed: { oldState: ConnectionState, newState: ConnectionState },
|
||||
notify_notify_received: { notify: MessageNotify }
|
||||
}
|
||||
|
||||
type NotifyPayloadType<K extends MessageNotify["type"]> = Extract<MessageNotify, { type: K }>["payload"];
|
||||
type CommandPayloadType<K extends MessageCommand["type"]> = Extract<MessageCommand, { type: K }>["payload"];
|
||||
|
||||
let tokenIndex = 0;
|
||||
export class ClientServiceConnection {
|
||||
readonly events: Registry<ClientServiceConnectionEvents>;
|
||||
|
@ -25,6 +27,7 @@ export class ClientServiceConnection {
|
|||
private connection: WebSocket;
|
||||
|
||||
private pendingCommands: {[key: string]: PendingCommand} = {};
|
||||
private notifyHandler: {[key: string]: ((event) => void)[]} = {};
|
||||
|
||||
constructor(reconnectInterval: number) {
|
||||
this.events = new Registry<ClientServiceConnectionEvents>();
|
||||
|
@ -34,6 +37,7 @@ export class ClientServiceConnection {
|
|||
destroy() {
|
||||
this.disconnect();
|
||||
this.events.destroy();
|
||||
this.notifyHandler = {};
|
||||
}
|
||||
|
||||
getState() : ConnectionState {
|
||||
|
@ -122,7 +126,7 @@ export class ClientServiceConnection {
|
|||
}
|
||||
}
|
||||
|
||||
async executeCommand(command: MessageCommand) : Promise<MessageCommandResult> {
|
||||
async executeMessageCommand(command: MessageCommand) : Promise<MessageCommandResult> {
|
||||
if(this.connectionState !== "connected") {
|
||||
return { type: "ConnectionClosed" };
|
||||
}
|
||||
|
@ -153,6 +157,83 @@ export class ClientServiceConnection {
|
|||
});
|
||||
}
|
||||
|
||||
async executeCommand<K extends MessageCommand["type"]>(command: K, payload: CommandPayloadType<K>) : Promise<MessageCommandResult> {
|
||||
return await this.executeMessageCommand({ type: command as any, payload: payload as any });
|
||||
}
|
||||
|
||||
registerNotifyHandler<K extends MessageNotify["type"]>(notify: K, callback: (notify: NotifyPayloadType<K>) => void) : () => void {
|
||||
const handler = this.notifyHandler[notify] || (this.notifyHandler[notify] = []);
|
||||
handler.push(callback);
|
||||
|
||||
return () => this.unregisterNotifyHandler(notify, callback as any);
|
||||
}
|
||||
|
||||
unregisterNotifyHandler<K extends MessageNotify["type"]>(callback: (notify: NotifyPayloadType<K>) => void);
|
||||
unregisterNotifyHandler<K extends MessageNotify["type"]>(notify: K, callback: (notify: NotifyPayloadType<K>) => void);
|
||||
unregisterNotifyHandler(notifyOrCallback, callback?) {
|
||||
if(typeof notifyOrCallback === "string") {
|
||||
const handler = this.notifyHandler[notifyOrCallback];
|
||||
if(!handler) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = handler.indexOf(callback);
|
||||
if(index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
handler.splice(index);
|
||||
if(handler.length === 0) {
|
||||
delete this.notifyHandler[notifyOrCallback];
|
||||
}
|
||||
} else {
|
||||
for(const key of Object.keys(this.notifyHandler)) {
|
||||
this.unregisterNotifyHandler(key as any, notifyOrCallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
catchNotify<K extends MessageNotify["type"]>(notify: K, filter?: (value: NotifyPayloadType<K>) => boolean) : () => ({ status: "success", value: NotifyPayloadType<K> } | { status: "fail" }) {
|
||||
/*
|
||||
* Note:
|
||||
* The current implementation allows the user to forget about the callback without causing any memory leaks.
|
||||
* The memory might still leak if the registered notify never triggered.
|
||||
*/
|
||||
const handlers = this.notifyHandler[notify] || (this.notifyHandler[notify] = []);
|
||||
const resultContainer = { result: null };
|
||||
|
||||
const handler = notify => {
|
||||
if(filter && !filter(notify)) {
|
||||
return;
|
||||
}
|
||||
|
||||
resultContainer.result = notify;
|
||||
unregisterHandler();
|
||||
};
|
||||
|
||||
const unregisterHandler = () => {
|
||||
const index = handlers.indexOf(handler);
|
||||
if(index !== -1) {
|
||||
handlers.remove(handler);
|
||||
}
|
||||
}
|
||||
|
||||
handlers.push(handler);
|
||||
return () => {
|
||||
unregisterHandler();
|
||||
if(resultContainer.result === null) {
|
||||
return {
|
||||
status: "fail"
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: "success",
|
||||
value: resultContainer.result
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleConnectFail() {
|
||||
this.disconnect();
|
||||
this.executeReconnect();
|
||||
|
@ -195,7 +276,16 @@ export class ClientServiceConnection {
|
|||
clientServiceLogger.logWarn("Received command result for unknown token: %o", data.token);
|
||||
}
|
||||
} else if(data.type === "Notify") {
|
||||
this.events.fire("notify_notify_received", { notify: data.notify });
|
||||
const handlers = this.notifyHandler[data.notify.type];
|
||||
if(typeof handlers !== "undefined") {
|
||||
for(const handler of [...handlers]) {
|
||||
try {
|
||||
handler(data.notify.payload);
|
||||
} catch (error) {
|
||||
clientServiceLogger.logError("Failed to invoke notify handler for %s: %o", data.notify, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
clientServiceLogger.logWarn("Received message with invalid type: %o", (data as any).type);
|
||||
}
|
||||
|
|
|
@ -34,6 +34,8 @@ export type MessageCommandResult =
|
|||
| { type: "InviteKeyInvalid"; fields: string }
|
||||
| { type: "InviteKeyNotFound" };
|
||||
|
||||
export type MessageCommandErrorResult = Exclude<MessageCommandResult, { type: "Success" }>;
|
||||
|
||||
export type MessageNotify =
|
||||
| { type: "NotifyClientsOnline"; payload: NotifyClientsOnline }
|
||||
| { type: "NotifyInviteCreated"; payload: NotifyInviteCreated }
|
||||
|
@ -53,7 +55,7 @@ export type CommandSessionInitializeAgent = { session_type: ClientSessionType; p
|
|||
|
||||
export type CommandSessionUpdateLocale = { ip_country: string | null; selected_locale: string | null; local_timestamp: number };
|
||||
|
||||
export type CommandInviteQueryInfo = { link_id: string };
|
||||
export type CommandInviteQueryInfo = { link_id: string, register_view: boolean };
|
||||
|
||||
export type CommandInviteLogAction = { click_type: number };
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
export { ClientServiceConfig, ClientServices, LocalAgent } from "./ClientService";
|
||||
export { ClientServiceLogger, setClientServiceLogger } from "./Logging";
|
||||
export { ClientSessionType } from "./Messages";
|
||||
export { ClientSessionType } from "./Messages";
|
||||
|
||||
export { ClientServiceInvite, InviteActionResult, InviteLinkInfo, InviteCreateResult } from "./ClientServiceInvite";
|
Loading…
Reference in New Issue