Skip to content

Commit

Permalink
Enable strict mode (#32)
Browse files Browse the repository at this point in the history
* Enable strict mode

* better check
  • Loading branch information
balloob authored Aug 9, 2018
1 parent 7dadf78 commit 749088d
Show file tree
Hide file tree
Showing 10 changed files with 107 additions and 63 deletions.
68 changes: 42 additions & 26 deletions lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,20 @@ type getAuthOptions = {
loadCache?: LoadCacheFunc;
};

type queryData = {
state?: string;
code?: string;
};

const CALLBACK_KEY = "auth_callback";

type QueryCallbackData =
| {}
| {
state: string;
code: string;
[CALLBACK_KEY]: string;
};

type OAuthState = {
hassUrl: string;
};

function genClientId() {
return `${location.protocol}//${location.host}/`;
}
Expand Down Expand Up @@ -62,7 +69,11 @@ function redirectAuthorize(hassUrl: string, state: string) {
);
}

async function tokenRequest(hassUrl: string, clientId: string, data: object) {
async function tokenRequest(
hassUrl: string,
clientId: string,
data: { [key: string]: string }
) {
const formData = new FormData();
formData.append("client_id", clientId);
Object.keys(data).forEach(key => {
Expand Down Expand Up @@ -100,39 +111,44 @@ function refreshAccessToken(
});
}

function encodeOauthState(state: object) {
function encodeOAuthState(state: OAuthState): string {
return btoa(JSON.stringify(state));
}

function decodeOauthState(encoded: string) {
function decodeOAuthState(encoded: string): OAuthState {
return JSON.parse(atob(encoded));
}

export class Auth {
_saveCache?: SaveCacheFunc;
expires: number;
hassUrl: string;
refresh_token: string;
access_token: string;
private _saveCache?: SaveCacheFunc;
data: AuthData;

constructor(data: AuthData, saveCache: SaveCacheFunc) {
Object.assign(this, data);
constructor(data: AuthData, saveCache?: SaveCacheFunc) {
this.data = data;
this._saveCache = saveCache;
}

get wsUrl() {
// Convert from http:// -> ws://, https:// -> wss://
return `ws${this.data.hassUrl.substr(4)}/api/websocket`;
}

get accessToken() {
return this.data.access_token;
}

get expired() {
// Token needs to be at least 10 seconds valid
return Date.now() > this.expires - 10000;
return Date.now() > this.data.expires - 10000;
}

async refreshAccessToken() {
const data = await refreshAccessToken(
this.hassUrl,
this.data = await refreshAccessToken(
this.data.hassUrl,
genClientId(),
this.refresh_token
this.data.refresh_token
);
Object.assign(this, data);
if (this._saveCache) this._saveCache(data);
if (this._saveCache) this._saveCache(this.data);
}
}

Expand All @@ -142,14 +158,14 @@ export default async function getAuth({
saveCache
}: getAuthOptions = {}): Promise<Auth> {
// Check if we came back from an authorize redirect
const query: queryData = parseQuery(location.search.substr(1));
const query = parseQuery<QueryCallbackData>(location.search.substr(1));

let data: AuthData;
let data: AuthData | undefined;

// Check if we got redirected here from authorize page
if (query[CALLBACK_KEY]) {
if ("auth_callback" in query) {
// Restore state
const state = decodeOauthState(query.state);
const state = decodeOAuthState(query.state);
try {
data = await fetchToken(state.hassUrl, genClientId(), query.code);
if (saveCache) saveCache(data);
Expand All @@ -168,7 +184,7 @@ export default async function getAuth({
if (!data && hassUrl) {
redirectAuthorize(
hassUrl,
encodeOauthState({
encodeOAuthState({
hassUrl
})
);
Expand Down
7 changes: 4 additions & 3 deletions lib/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import createCollection from "./collection";
import { HassConfig } from "./types";
import { Connection } from "./connection";
import Store from "./store";

type ComponentLoadedEvent = {
data: {
Expand All @@ -11,16 +12,16 @@ type ComponentLoadedEvent = {
function processComponentLoaded(
state: HassConfig,
event: ComponentLoadedEvent
): Partial<HassConfig> {
): Partial<HassConfig> | null {
if (state === undefined) return null;

return {
components: state.components.concat(event.data.component)
};
}

const fetchConfig = conn => conn.getConfig();
const subscribeUpdates = (conn, store) =>
const fetchConfig = (conn: Connection) => conn.getConfig();
const subscribeUpdates = (conn: Connection, store: Store<HassConfig>) =>
conn.subscribeEvents(
store.action(processComponentLoaded),
"component_loaded"
Expand Down
31 changes: 19 additions & 12 deletions lib/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,19 @@ import { ERR_INVALID_AUTH, ERR_CONNECTION_LOST } from "./const";
import {
ConnectionOptions,
HassEvent,
HassEntities,
HassServices,
HassConfig
HassConfig,
MessageBase,
HassEntity
} from "./types";
import createSocket from "./socket";

const DEBUG = false;

type EventListener = (conn: Connection, eventData?: any) => void;

type Events = "ready" | "disconnected" | "reconnect-error";

type WebSocketPongResponse = {
id: number;
type: "pong";
Expand Down Expand Up @@ -61,9 +64,10 @@ export class Connection {
[eventType: string]: EventListener[];
};
closeRequested: boolean;
// @ts-ignore: incorrectly claiming it's not set in constructor.
socket: WebSocket;

constructor(options: ConnectionOptions) {
constructor(socket: WebSocket, options: ConnectionOptions) {
// connection options
// - setupRetry: amount of ms to retry when unable to connect on initial setup
// - createSocket: create a new Socket connection
Expand All @@ -78,6 +82,7 @@ export class Connection {
this.closeRequested = false;

this._handleClose = this._handleClose.bind(this);
this.setSocket(socket);
}

setSocket(socket: WebSocket) {
Expand Down Expand Up @@ -109,7 +114,7 @@ export class Connection {
}
}

addEventListener(eventType, callback) {
addEventListener(eventType: Events, callback: EventListener) {
let listeners = this.eventListeners[eventType];

if (!listeners) {
Expand All @@ -119,7 +124,7 @@ export class Connection {
listeners.push(callback);
}

removeEventListener(eventType, callback) {
removeEventListener(eventType: Events, callback: EventListener) {
const listeners = this.eventListeners[eventType];

if (!listeners) {
Expand All @@ -133,7 +138,7 @@ export class Connection {
}
}

fireEvent(eventType, eventData?) {
fireEvent(eventType: Events, eventData?: any) {
(this.eventListeners[eventType] || []).forEach(callback =>
callback(this, eventData)
);
Expand All @@ -145,7 +150,7 @@ export class Connection {
}

getStates() {
return this.sendMessagePromise<HassEntities>(messages.states());
return this.sendMessagePromise<HassEntity[]>(messages.states());
}

getServices() {
Expand All @@ -156,7 +161,7 @@ export class Connection {
return this.sendMessagePromise<HassConfig>(messages.config());
}

callService(domain, service, serviceData) {
callService(domain: string, service: string, serviceData?: object) {
return this.sendMessagePromise(
messages.callService(domain, service, serviceData)
);
Expand Down Expand Up @@ -196,7 +201,7 @@ export class Connection {
return this.sendMessagePromise(messages.ping());
}

sendMessage(message, commandId?: number): void {
sendMessage(message: MessageBase, commandId?: number): void {
if (DEBUG) {
console.log("Sending", message);
}
Expand All @@ -209,7 +214,10 @@ export class Connection {
this.socket.send(JSON.stringify(message));
}

sendMessagePromise<Result>(message, commandId?: number): Promise<Result> {
sendMessagePromise<Result>(
message: MessageBase,
commandId?: number
): Promise<Result> {
return new Promise((resolve, reject) => {
if (!commandId) {
commandId = this._genCmdId();
Expand Down Expand Up @@ -312,7 +320,6 @@ export default async function createConnection(
options
);
const socket = await connOptions.createSocket(connOptions);
const conn = new Connection(connOptions);
conn.setSocket(socket);
const conn = new Connection(socket, connOptions);
return conn;
}
4 changes: 2 additions & 2 deletions lib/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ function processEvent(store: Store<HassEntities>, event: StateChangedEvent) {
}
}

async function fetchEntities(conn) {
async function fetchEntities(conn: Connection): Promise<HassEntities> {
const states = await conn.getStates();
const entities = {};
const entities: HassEntities = {};
for (let i = 0; i < states.length; i++) {
const state = states[i];
entities[state.entity_id] = state;
Expand Down
4 changes: 2 additions & 2 deletions lib/services.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import createCollection from "./collection";
import { HassServices } from "./types";
import { HassServices, HassDomainServices } from "./types";
import { Connection } from "./connection";
import Store from "./store";

Expand Down Expand Up @@ -43,7 +43,7 @@ function processServiceRemoved(

if (!curDomainInfo || !(service in curDomainInfo)) return null;

const domainInfo = {};
const domainInfo: HassDomainServices = {};
Object.keys(curDomainInfo).forEach(sKey => {
if (sKey !== service) domainInfo[sKey] = curDomainInfo[sKey];
});
Expand Down
20 changes: 14 additions & 6 deletions lib/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,35 @@ import {
MSG_TYPE_AUTH_OK,
MSG_TYPE_AUTH_REQUIRED,
ERR_INVALID_AUTH,
ERR_CANNOT_CONNECT
ERR_CANNOT_CONNECT,
ERR_HASS_HOST_REQUIRED
} from "./const";
import { MSG_TYPE_AUTH_INVALID } from "./const";
import { ConnectionOptions } from "./types";
import { ConnectionOptions, Error } from "./types";
import { authAccessToken } from "./messages";

const DEBUG = false;

export default function createSocket(
options: ConnectionOptions
): Promise<WebSocket> {
if (!options.auth) {
throw ERR_HASS_HOST_REQUIRED;
}
const auth = options.auth;

// Convert from http:// -> ws://, https:// -> wss://
const url = `ws${auth.hassUrl.substr(4)}/api/websocket`;
const url = auth.wsUrl;

if (DEBUG) {
console.log("[Auth phase] Initializing", url);
}

function connect(triesLeft, promResolve, promReject) {
function connect(
triesLeft: number,
promResolve: (socket: WebSocket) => void,
promReject: (err: Error) => void
) {
if (DEBUG) {
console.log("[Auth Phase] New connection", url);
}
Expand Down Expand Up @@ -63,7 +71,7 @@ export default function createSocket(
);
};

const handleMessage = async event => {
const handleMessage = async (event: MessageEvent) => {
const message = JSON.parse(event.data);

if (DEBUG) {
Expand All @@ -73,7 +81,7 @@ export default function createSocket(
case MSG_TYPE_AUTH_REQUIRED:
try {
if (auth.expired) await auth.refreshAccessToken();
socket.send(JSON.stringify(authAccessToken(auth.access_token)));
socket.send(JSON.stringify(authAccessToken(auth.accessToken)));
} catch (err) {
// Refresh token failed
invalidAuth = true;
Expand Down
11 changes: 6 additions & 5 deletions lib/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ export default class Store<State> {
action(
action: (
state: State,
...args
...args: any[]
) => Partial<State> | Promise<Partial<State>> | null
) {
const apply = (result: Partial<State>) => this.setState(result, false);

// Note: perf tests verifying this implementation: https://esbench.com/bench/5a295e6299634800a0349500
return (...args) => {
const ret = action(this.state, ...args);
return (...args: any[]) => {
const ret = action(this.state as State, ...args);
if (ret != null) {
return "then" in ret ? ret.then(apply) : apply(ret);
}
Expand Down Expand Up @@ -70,11 +70,12 @@ export default class Store<State> {
* @function
*/
unsubscribe(listener: Listener<State>) {
let toFind: Listener<State> | null = listener;
const out = [];
const listeners = this.listeners;
for (let i = 0; i < listeners.length; i++) {
if (listeners[i] === listener) {
listener = null;
if (listeners[i] === toFind) {
toFind = null;
} else {
out.push(listeners[i]);
}
Expand Down
Loading

0 comments on commit 749088d

Please sign in to comment.