Better handling of expired access tokens
This commit is contained in:
parent
f679af1a35
commit
f8a1dd0a8c
@ -2,8 +2,8 @@ import { action, autorun, observable, runInAction, when } from "mobx";
|
|||||||
import { update } from "serializr";
|
import { update } from "serializr";
|
||||||
|
|
||||||
import { TokenStore } from "@client/state/TokenStore";
|
import { TokenStore } from "@client/state/TokenStore";
|
||||||
import { UserStore } from "@client/state/UserStore";
|
|
||||||
import { ErrorCode } from "@common/ErrorCode";
|
import { ErrorCode } from "@common/ErrorCode";
|
||||||
|
import { IUser } from "@common/httpApi";
|
||||||
import * as rpc from "@common/jsonRpc";
|
import * as rpc from "@common/jsonRpc";
|
||||||
import logger from "@common/logger";
|
import logger from "@common/logger";
|
||||||
import * as deviceRequests from "@common/sprinklersRpc/deviceRequests";
|
import * as deviceRequests from "@common/sprinklersRpc/deviceRequests";
|
||||||
@ -11,6 +11,7 @@ import * as s from "@common/sprinklersRpc/index";
|
|||||||
import * as schema from "@common/sprinklersRpc/schema/index";
|
import * as schema from "@common/sprinklersRpc/schema/index";
|
||||||
import { seralizeRequest } from "@common/sprinklersRpc/schema/requests";
|
import { seralizeRequest } from "@common/sprinklersRpc/schema/requests";
|
||||||
import * as ws from "@common/sprinklersRpc/websocketData";
|
import * as ws from "@common/sprinklersRpc/websocketData";
|
||||||
|
import { DefaultEvents, TypedEventEmitter } from "@common/TypedEventEmitter";
|
||||||
|
|
||||||
const log = logger.child({ source: "websocket" });
|
const log = logger.child({ source: "websocket" });
|
||||||
|
|
||||||
@ -83,7 +84,13 @@ export class WSSprinklersDevice extends s.SprinklersDevice {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WebSocketRpcClient implements s.SprinklersRPC {
|
export interface WebSocketRpcClientEvents extends DefaultEvents {
|
||||||
|
newUserData(userData: IUser): void;
|
||||||
|
rpcError(error: ws.RpcError): void;
|
||||||
|
tokenError(error: ws.RpcError): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WebSocketRpcClient extends TypedEventEmitter<WebSocketRpcClientEvents> implements s.SprinklersRPC {
|
||||||
readonly webSocketUrl: string;
|
readonly webSocketUrl: string;
|
||||||
|
|
||||||
devices: Map<string, WSSprinklersDevice> = new Map();
|
devices: Map<string, WSSprinklersDevice> = new Map();
|
||||||
@ -94,7 +101,6 @@ export class WebSocketRpcClient implements s.SprinklersRPC {
|
|||||||
authenticated: boolean = false;
|
authenticated: boolean = false;
|
||||||
|
|
||||||
tokenStore: TokenStore;
|
tokenStore: TokenStore;
|
||||||
userStore: UserStore;
|
|
||||||
|
|
||||||
private nextRequestId = Math.round(Math.random() * 1000000);
|
private nextRequestId = Math.round(Math.random() * 1000000);
|
||||||
private responseCallbacks: ws.ServerResponseHandlers = {};
|
private responseCallbacks: ws.ServerResponseHandlers = {};
|
||||||
@ -104,12 +110,18 @@ export class WebSocketRpcClient implements s.SprinklersRPC {
|
|||||||
return this.connectionState.isServerConnected || false;
|
return this.connectionState.isServerConnected || false;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(tokenStore: TokenStore, userStore: UserStore, webSocketUrl: string = DEFAULT_URL) {
|
constructor(tokenStore: TokenStore, webSocketUrl: string = DEFAULT_URL) {
|
||||||
|
super();
|
||||||
this.webSocketUrl = webSocketUrl;
|
this.webSocketUrl = webSocketUrl;
|
||||||
this.tokenStore = tokenStore;
|
this.tokenStore = tokenStore;
|
||||||
this.userStore = userStore;
|
|
||||||
this.connectionState.clientToServer = false;
|
this.connectionState.clientToServer = false;
|
||||||
this.connectionState.serverToBroker = false;
|
this.connectionState.serverToBroker = false;
|
||||||
|
|
||||||
|
this.on("rpcError", (err: ws.RpcError) => {
|
||||||
|
if (err.code === ErrorCode.BadToken) {
|
||||||
|
this.emit("tokenError", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
@ -149,13 +161,17 @@ export class WebSocketRpcClient implements s.SprinklersRPC {
|
|||||||
&& this.tokenStore.accessToken.isValid, async () => {
|
&& this.tokenStore.accessToken.isValid, async () => {
|
||||||
try {
|
try {
|
||||||
const res = await this.authenticate(this.tokenStore.accessToken.token!);
|
const res = await this.authenticate(this.tokenStore.accessToken.token!);
|
||||||
this.authenticated = res.authenticated;
|
runInAction("authenticateSuccess", () => {
|
||||||
|
this.authenticated = res.authenticated;
|
||||||
|
});
|
||||||
logger.info({ user: res.user }, "authenticated websocket connection");
|
logger.info({ user: res.user }, "authenticated websocket connection");
|
||||||
this.userStore.receiveUserData(res.user);
|
this.emit("newUserData", res.user);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error({ err }, "error authenticating websocket connection");
|
logger.error({ err }, "error authenticating websocket connection");
|
||||||
// TODO message?
|
// TODO message?
|
||||||
this.authenticated = false;
|
runInAction("authenticateSuccess", () => {
|
||||||
|
this.authenticated = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -167,17 +183,13 @@ export class WebSocketRpcClient implements s.SprinklersRPC {
|
|||||||
code: ErrorCode.ServerDisconnected,
|
code: ErrorCode.ServerDisconnected,
|
||||||
message: "the server is not connected",
|
message: "the server is not connected",
|
||||||
};
|
};
|
||||||
throw error;
|
throw new ws.RpcError("the server is not connected", ErrorCode.ServerDisconnected);
|
||||||
}
|
}
|
||||||
const requestData = seralizeRequest(request);
|
const requestData = seralizeRequest(request);
|
||||||
const data: ws.IDeviceCallRequest = { deviceId, data: requestData };
|
const data: ws.IDeviceCallRequest = { deviceId, data: requestData };
|
||||||
const resData = await this.makeRequest("deviceCall", data);
|
const resData = await this.makeRequest("deviceCall", data);
|
||||||
if (resData.data.result === "error") {
|
if (resData.data.result === "error") {
|
||||||
throw {
|
throw new ws.RpcError(resData.data.message, resData.data.code, resData.data);
|
||||||
code: resData.data.code,
|
|
||||||
message: resData.data.message,
|
|
||||||
data: resData.data,
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
return resData.data;
|
return resData.data;
|
||||||
}
|
}
|
||||||
@ -194,21 +206,22 @@ export class WebSocketRpcClient implements s.SprinklersRPC {
|
|||||||
if (response.result === "success") {
|
if (response.result === "success") {
|
||||||
resolve(response.data);
|
resolve(response.data);
|
||||||
} else {
|
} else {
|
||||||
reject(response.error);
|
const { error } = response;
|
||||||
|
reject(new ws.RpcError(error.message, error.code, error.data));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
timeoutHandle = window.setTimeout(() => {
|
timeoutHandle = window.setTimeout(() => {
|
||||||
delete this.responseCallbacks[id];
|
delete this.responseCallbacks[id];
|
||||||
const res: ws.ErrorData = {
|
reject(new ws.RpcError("the request timed out", ErrorCode.Timeout));
|
||||||
result: "error", error: {
|
|
||||||
code: ErrorCode.Timeout,
|
|
||||||
message: "the request timed out",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
reject(res);
|
|
||||||
}, TIMEOUT_MS);
|
}, TIMEOUT_MS);
|
||||||
this.sendRequest(id, method, params);
|
this.sendRequest(id, method, params);
|
||||||
});
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err instanceof ws.RpcError) {
|
||||||
|
this.emit("rpcError", err);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendMessage(data: ws.ClientMessage) {
|
private sendMessage(data: ws.ClientMessage) {
|
||||||
@ -230,7 +243,7 @@ export class WebSocketRpcClient implements s.SprinklersRPC {
|
|||||||
|
|
||||||
private _connect() {
|
private _connect() {
|
||||||
if (this.socket != null &&
|
if (this.socket != null &&
|
||||||
(this.socket.readyState === WebSocket.CLOSED)) {
|
(this.socket.readyState === WebSocket.OPEN)) {
|
||||||
this.tryAuthenticate();
|
this.tryAuthenticate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -242,6 +255,7 @@ export class WebSocketRpcClient implements s.SprinklersRPC {
|
|||||||
this.socket.onmessage = this.onMessage.bind(this);
|
this.socket.onmessage = this.onMessage.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
private onOpen() {
|
private onOpen() {
|
||||||
log.info("established websocket connection");
|
log.info("established websocket connection");
|
||||||
this.connectionState.clientToServer = true;
|
this.connectionState.clientToServer = true;
|
||||||
@ -250,6 +264,7 @@ export class WebSocketRpcClient implements s.SprinklersRPC {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* tslint:disable-next-line:member-ordering */
|
/* tslint:disable-next-line:member-ordering */
|
||||||
|
@action
|
||||||
private onDisconnect = action(() => {
|
private onDisconnect = action(() => {
|
||||||
this.connectionState.serverToBroker = null;
|
this.connectionState.serverToBroker = null;
|
||||||
this.connectionState.clientToServer = false;
|
this.connectionState.clientToServer = false;
|
||||||
@ -263,12 +278,11 @@ export class WebSocketRpcClient implements s.SprinklersRPC {
|
|||||||
this.reconnectTimer = window.setTimeout(this._reconnect, RECONNECT_TIMEOUT_MS);
|
this.reconnectTimer = window.setTimeout(this._reconnect, RECONNECT_TIMEOUT_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
private onError(event: Event) {
|
private onError(event: Event) {
|
||||||
log.error({ event }, "websocket error");
|
log.error({ event }, "websocket error");
|
||||||
action(() => {
|
this.connectionState.serverToBroker = null;
|
||||||
this.connectionState.serverToBroker = null;
|
this.connectionState.clientToServer = false;
|
||||||
this.connectionState.clientToServer = false;
|
|
||||||
});
|
|
||||||
this.onDisconnect();
|
this.onDisconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -335,4 +349,4 @@ class WSClientNotificationHandlers implements ws.ServerNotificationHandlers {
|
|||||||
error(data: ws.IError) {
|
error(data: ws.IError) {
|
||||||
log.warn({ err: data }, "server error");
|
log.warn({ err: data }, "server error");
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { createBrowserHistory, History } from "history";
|
import { createBrowserHistory, History } from "history";
|
||||||
import { computed, configure } from "mobx";
|
import { computed, configure, when } from "mobx";
|
||||||
import { RouterStore, syncHistoryWithStore } from "mobx-react-router";
|
import { RouterStore, syncHistoryWithStore } from "mobx-react-router";
|
||||||
|
|
||||||
import { WebSocketRpcClient } from "@client/sprinklersRpc/WebSocketRpcClient";
|
import { WebSocketRpcClient } from "@client/sprinklersRpc/WebSocketRpcClient";
|
||||||
@ -8,16 +8,37 @@ import { UiStore } from "@client/state/UiStore";
|
|||||||
import { UserStore } from "@client/state/UserStore";
|
import { UserStore } from "@client/state/UserStore";
|
||||||
import ApiError from "@common/ApiError";
|
import ApiError from "@common/ApiError";
|
||||||
import { ErrorCode } from "@common/ErrorCode";
|
import { ErrorCode } from "@common/ErrorCode";
|
||||||
|
import { IUser } from "@common/httpApi";
|
||||||
import log from "@common/logger";
|
import log from "@common/logger";
|
||||||
|
import { TypedEventEmitter, DefaultEvents } from "@common/TypedEventEmitter";
|
||||||
|
|
||||||
export default class AppState {
|
interface AppEvents extends DefaultEvents {
|
||||||
|
checkToken(): void;
|
||||||
|
hasToken(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class AppState extends TypedEventEmitter<AppEvents> {
|
||||||
history: History = createBrowserHistory();
|
history: History = createBrowserHistory();
|
||||||
routerStore = new RouterStore();
|
routerStore = new RouterStore();
|
||||||
uiStore = new UiStore();
|
uiStore = new UiStore();
|
||||||
userStore = new UserStore();
|
userStore = new UserStore();
|
||||||
httpApi = new HttpApi();
|
httpApi = new HttpApi();
|
||||||
tokenStore = this.httpApi.tokenStore;
|
tokenStore = this.httpApi.tokenStore;
|
||||||
sprinklersRpc = new WebSocketRpcClient(this.tokenStore, this.userStore);
|
sprinklersRpc = new WebSocketRpcClient(this.tokenStore);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.sprinklersRpc.on("newUserData", this.userStore.receiveUserData);
|
||||||
|
this.sprinklersRpc.on("tokenError", this.checkToken);
|
||||||
|
this.httpApi.on("tokenError", this.checkToken);
|
||||||
|
|
||||||
|
this.on("checkToken", this.doCheckToken);
|
||||||
|
|
||||||
|
this.on("hasToken", () => {
|
||||||
|
when(() => !this.tokenStore.accessToken.isValid, this.checkToken);
|
||||||
|
this.sprinklersRpc.start();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@computed get isLoggedIn() {
|
@computed get isLoggedIn() {
|
||||||
return this.tokenStore.accessToken.isValid;
|
return this.tokenStore.accessToken.isValid;
|
||||||
@ -29,15 +50,20 @@ export default class AppState {
|
|||||||
});
|
});
|
||||||
|
|
||||||
syncHistoryWithStore(this.history, this.routerStore);
|
syncHistoryWithStore(this.history, this.routerStore);
|
||||||
this.tokenStore.loadLocalStorage();
|
await this.tokenStore.loadLocalStorage();
|
||||||
|
|
||||||
await this.checkToken();
|
await this.checkToken();
|
||||||
await this.sprinklersRpc.start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkToken() {
|
checkToken = () => {
|
||||||
const { tokenStore: { accessToken, refreshToken } } = this.httpApi;
|
this.emit("checkToken");
|
||||||
|
}
|
||||||
|
|
||||||
|
private doCheckToken = async () => {
|
||||||
|
const { accessToken, refreshToken } = this.tokenStore;
|
||||||
|
accessToken.updateCurrentTime();
|
||||||
if (accessToken.isValid) { // if the access token is valid, we are good
|
if (accessToken.isValid) { // if the access token is valid, we are good
|
||||||
|
this.emit("hasToken");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!refreshToken.isValid) { // if the refresh token is not valid, need to login again
|
if (!refreshToken.isValid) { // if the refresh token is not valid, need to login again
|
||||||
@ -46,6 +72,7 @@ export default class AppState {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await this.httpApi.grantRefresh();
|
await this.httpApi.grantRefresh();
|
||||||
|
this.emit("hasToken");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof ApiError && err.code === ErrorCode.BadToken) {
|
if (err instanceof ApiError && err.code === ErrorCode.BadToken) {
|
||||||
log.warn({ err }, "refresh is bad for some reason, erasing");
|
log.warn({ err }, "refresh is bad for some reason, erasing");
|
||||||
|
@ -3,11 +3,17 @@ import ApiError from "@common/ApiError";
|
|||||||
import { ErrorCode } from "@common/ErrorCode";
|
import { ErrorCode } from "@common/ErrorCode";
|
||||||
import { TokenGrantPasswordRequest, TokenGrantRefreshRequest, TokenGrantResponse } from "@common/httpApi";
|
import { TokenGrantPasswordRequest, TokenGrantRefreshRequest, TokenGrantResponse } from "@common/httpApi";
|
||||||
import log from "@common/logger";
|
import log from "@common/logger";
|
||||||
|
import { DefaultEvents, TypedEventEmitter } from "@common/TypedEventEmitter";
|
||||||
import { runInAction } from "mobx";
|
import { runInAction } from "mobx";
|
||||||
|
|
||||||
export { ApiError };
|
export { ApiError };
|
||||||
|
|
||||||
export default class HttpApi {
|
interface HttpApiEvents extends DefaultEvents {
|
||||||
|
error(err: ApiError): void;
|
||||||
|
tokenError(err: ApiError): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class HttpApi extends TypedEventEmitter<HttpApiEvents> {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
|
|
||||||
tokenStore: TokenStore;
|
tokenStore: TokenStore;
|
||||||
@ -20,36 +26,53 @@ export default class HttpApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor(baseUrl: string = `${location.protocol}//${location.hostname}:${location.port}/api`) {
|
constructor(baseUrl: string = `${location.protocol}//${location.hostname}:${location.port}/api`) {
|
||||||
|
super();
|
||||||
while (baseUrl.charAt(baseUrl.length - 1) === "/") {
|
while (baseUrl.charAt(baseUrl.length - 1) === "/") {
|
||||||
baseUrl = baseUrl.substring(0, baseUrl.length - 1);
|
baseUrl = baseUrl.substring(0, baseUrl.length - 1);
|
||||||
}
|
}
|
||||||
this.baseUrl = baseUrl;
|
this.baseUrl = baseUrl;
|
||||||
|
|
||||||
this.tokenStore = new TokenStore();
|
this.tokenStore = new TokenStore();
|
||||||
|
|
||||||
|
this.on("error", (err: ApiError) => {
|
||||||
|
if (err.code === ErrorCode.BadToken) {
|
||||||
|
this.emit("tokenError", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async makeRequest(url: string, options?: RequestInit, body?: any): Promise<any> {
|
async makeRequest(url: string, options?: RequestInit, body?: any): Promise<any> {
|
||||||
options = options || {};
|
|
||||||
options = {
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...this.authorizationHeader,
|
|
||||||
...options.headers || {},
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
...options,
|
|
||||||
};
|
|
||||||
const response = await fetch(this.baseUrl + url, options);
|
|
||||||
let responseBody: any;
|
|
||||||
try {
|
try {
|
||||||
responseBody = await response.json() || {};
|
options = options || {};
|
||||||
} catch (e) {
|
options = {
|
||||||
throw new ApiError("Invalid JSON response", ErrorCode.Internal, e);
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...this.authorizationHeader,
|
||||||
|
...options.headers || {},
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
let response: Response;
|
||||||
|
try {
|
||||||
|
response = await fetch(this.baseUrl + url, options);
|
||||||
|
} catch (err) {
|
||||||
|
throw new ApiError("Http request error", ErrorCode.Internal, err);
|
||||||
|
}
|
||||||
|
let responseBody: any;
|
||||||
|
try {
|
||||||
|
responseBody = await response.json() || {};
|
||||||
|
} catch (e) {
|
||||||
|
throw new ApiError("Invalid JSON response", ErrorCode.Internal, e);
|
||||||
|
}
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new ApiError(responseBody.message || response.statusText, responseBody.code, responseBody.data);
|
||||||
|
}
|
||||||
|
return responseBody;
|
||||||
|
} catch (err) {
|
||||||
|
this.emit("error", err);
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
if (!response.ok) {
|
|
||||||
throw new ApiError(responseBody.message || response.statusText, responseBody.code, responseBody.data);
|
|
||||||
}
|
|
||||||
return responseBody;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async grantPassword(username: string, password: string) {
|
async grantPassword(username: string, password: string) {
|
||||||
|
@ -27,7 +27,7 @@ export class Token<TClaims extends TokenClaims = TokenClaims> {
|
|||||||
return this.token;
|
return this.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateCurrentTime = (reportChanged: boolean = true) => {
|
updateCurrentTime = (reportChanged: boolean = true) => {
|
||||||
if (reportChanged) {
|
if (reportChanged) {
|
||||||
this.isExpiredAtom.reportChanged();
|
this.isExpiredAtom.reportChanged();
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import { action, observable } from "mobx";
|
|||||||
export class UserStore {
|
export class UserStore {
|
||||||
@observable userData: IUser | null = null;
|
@observable userData: IUser | null = null;
|
||||||
|
|
||||||
@action
|
@action.bound
|
||||||
receiveUserData(userData: IUser) {
|
receiveUserData(userData: IUser) {
|
||||||
this.userData = userData;
|
this.userData = userData;
|
||||||
}
|
}
|
||||||
|
48
common/TypedEventEmitter.ts
Normal file
48
common/TypedEventEmitter.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { EventEmitter } from "events";
|
||||||
|
|
||||||
|
type TEventName = string | symbol;
|
||||||
|
|
||||||
|
type AnyListener = (...args: any[]) => void;
|
||||||
|
|
||||||
|
type Arguments<TListener> = TListener extends (...args: infer TArgs) => any ? TArgs : any[];
|
||||||
|
type Listener<TEvents, TEvent extends keyof TEvents> = TEvents[TEvent] extends (...args: infer TArgs) => any ?
|
||||||
|
(...args: TArgs) => void : AnyListener;
|
||||||
|
|
||||||
|
export interface DefaultEvents {
|
||||||
|
newListener: (event: TEventName, listener: AnyListener) => void;
|
||||||
|
removeListener: (event: TEventName, listener: AnyListener) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AnyEvents = DefaultEvents & {
|
||||||
|
[event in TEventName]: any[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type IEventSubscriber<TEvents extends DefaultEvents, This> =
|
||||||
|
<TEvent extends keyof TEvents & TEventName>(event: TEvent, listener: Listener<TEvents, TEvent>) => This;
|
||||||
|
|
||||||
|
// tslint:disable:ban-types
|
||||||
|
|
||||||
|
interface ITypedEventEmitter<TEvents extends DefaultEvents = AnyEvents> {
|
||||||
|
on: IEventSubscriber<TEvents, this>;
|
||||||
|
off: IEventSubscriber<TEvents, this>;
|
||||||
|
once: IEventSubscriber<TEvents, this>;
|
||||||
|
addListener: IEventSubscriber<TEvents, this>;
|
||||||
|
removeListener: IEventSubscriber<TEvents, this>;
|
||||||
|
prependListener: IEventSubscriber<TEvents, this>;
|
||||||
|
prependOnceListener: IEventSubscriber<TEvents, this>;
|
||||||
|
|
||||||
|
emit<TEvent extends keyof TEvents & TEventName>(event: TEvent, ...args: Arguments<TEvents[TEvent]>): boolean;
|
||||||
|
listeners<TEvent extends keyof TEvents & TEventName>(event: TEvent): Function[];
|
||||||
|
rawListeners<TEvent extends keyof TEvents & TEventName>(event: TEvent): Function[];
|
||||||
|
eventNames(): Array<keyof TEvents | TEventName>;
|
||||||
|
setMaxListeners(maxListeners: number): this;
|
||||||
|
getMaxListeners(): number;
|
||||||
|
listenerCount<TEvent extends keyof TEvents & TEventName>(event: TEvent): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TypedEventEmitter = EventEmitter as {
|
||||||
|
new<TEvents extends DefaultEvents = AnyEvents>(): TypedEventEmitter<TEvents>,
|
||||||
|
};
|
||||||
|
type TypedEventEmitter<TEvents extends DefaultEvents = AnyEvents> = ITypedEventEmitter<TEvents>;
|
||||||
|
|
||||||
|
export { TypedEventEmitter };
|
@ -45,6 +45,7 @@ export class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async insertData() {
|
async insertData() {
|
||||||
|
this.conn.subscribers
|
||||||
const NUM = 100;
|
const NUM = 100;
|
||||||
const users: User[] = [];
|
const users: User[] = [];
|
||||||
for (let i = 0; i < NUM; i++) {
|
for (let i = 0; i < NUM; i++) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user