Auto-unsubscribe on disconnection (#50)

* Auto-unsubscribe on disconnection

* Use subscriptions for all in popup

* Cleanup state setters

* Plural for address(es)
This commit is contained in:
Jaco Greeff
2019-06-03 18:20:36 +02:00
committed by GitHub
parent 0ac194b187
commit f4413a209b
10 changed files with 170 additions and 72 deletions
+1 -1
View File
@@ -109,7 +109,7 @@ export async function web3Accounts (): Promise<Array<InjectedAccountWithMeta>> {
const addresses = accounts.map(({ address }) => address);
console.log(`web3Accounts: Found ${accounts.length} addresses ${addresses.join(', ')}`);
console.log(`web3Accounts: Found ${accounts.length} address${accounts.length !== 1 ? 'es' : ''}: ${addresses.join(', ')}`);
return accounts;
}
+6 -12
View File
@@ -10,7 +10,7 @@ import { Route, Switch } from 'react-router';
import { Loading } from '../components';
import { AccountContext, ActionContext, AuthorizeContext, SigningContext } from '../components/contexts';
import { getAccounts, getAuthRequests, getSignRequests } from '../messaging';
import { subscribeAccounts, subscribeAuthorize, subscribeSigning } from '../messaging';
import Accounts from './Accounts';
import Authorize from './Authorize';
import Create from './Create';
@@ -30,23 +30,17 @@ export default function Popup (props: Props) {
const onAction = (to?: string): void => {
setWelcomeDone(window.localStorage.getItem('welcome_read') === 'ok');
// loads all accounts & requests (this is passed through to children to trigger changes)
Promise
.all([getAccounts(), getAuthRequests(), getSignRequests()])
.then(([accounts, authRequests, signRequests]) => {
setAccounts(accounts);
setAuthRequests(authRequests);
setSignRequests(signRequests);
})
.catch(console.error);
if (to) {
window.location.hash = to;
}
};
useEffect((): void => {
// with an empty state, load the accounts & requests
Promise.all([
subscribeAccounts(setAccounts),
subscribeAuthorize(setAuthRequests),
subscribeSigning(setSignRequests)
]).catch(console.error);
onAction();
}, []);
+27 -8
View File
@@ -9,11 +9,14 @@ import { KeypairType } from '@polkadot/util-crypto/types';
import extension from 'extensionizer';
import { PORT_POPUP } from '@polkadot/extension/defaults';
type Handler = {
resolve: (data: any) => void,
reject: (error: Error) => void,
subscriber?: (data: any) => void
};
type Handlers = {
[index: string]: {
resolve: (data: any) => void,
reject: (error: Error) => void
}
[index: string]: Handler
};
const port = extension.runtime.connect({ name: PORT_POPUP });
@@ -29,20 +32,24 @@ port.onMessage.addListener((data) => {
return;
}
delete handlers[data.id];
if (!handler.subscriber) {
delete handlers[data.id];
}
if (data.error) {
if (data.subscription) {
(handler.subscriber as Function)(data.subscription);
} else if (data.error) {
handler.reject(new Error(data.error));
} else {
handler.resolve(data.response);
}
});
function sendMessage (message: MessageTypes, request: any = {}): Promise<any> {
function sendMessage (message: MessageTypes, request: any = {}, subscriber?: (data: any) => void): Promise<any> {
return new Promise((resolve, reject) => {
const id = `${Date.now()}.${++idCounter}`;
handlers[id] = { resolve, reject };
handlers[id] = { resolve, reject, subscriber };
port.postMessage({ id, message, request });
});
@@ -92,6 +99,18 @@ export async function createSeed (length?: number, type?: KeypairType): Promise<
return sendMessage('seed.create', { length, type });
}
export async function subscribeAccounts (cb: (accounts: Array<KeyringJson>) => void): Promise<boolean> {
return sendMessage('accounts.subscribe', {}, cb);
}
export async function subscribeAuthorize (cb: (accounts: Array<AuthorizeRequest>) => void): Promise<boolean> {
return sendMessage('authorize.subscribe', {}, cb);
}
export async function subscribeSigning (cb: (accounts: Array<SigningRequest>) => void): Promise<boolean> {
return sendMessage('signing.subscribe', {}, cb);
}
export async function validateSeed (seed: string, type?: KeypairType): Promise<{ address: string, seed: string }> {
return sendMessage('seed.validate', { seed, type });
}
@@ -2,6 +2,7 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
import { KeyringJson } from '@polkadot/ui-keyring/types';
import { AuthorizeRequest, MessageTypes, MessageAccountCreate, MessageAccountEdit, MessageAuthorizeApprove, MessageAuthorizeReject, MessageExtrinsicSignApprove, MessageExtrinsicSignCancel, MessageSeedCreate, MessageSeedCreate$Response, MessageSeedValidate, MessageSeedValidate$Response, MessageAccountForget, SigningRequest } from '../types';
@@ -12,10 +13,15 @@ import { assert, u8aToHex } from '@polkadot/util';
import RawPayload from '../RawPayload';
import State from './State';
import { createSubscription, unsubscribe } from './subscriptions';
const SEED_DEFAULT_LENGTH = 12;
const SEED_LENGTHS = [12, 24];
function transformAccounts (accounts: SubjectInfo): Array<KeyringJson> {
return Object.values(accounts).map(({ json }) => json);
}
export default class Extension {
private _state: State;
@@ -46,9 +52,22 @@ export default class Extension {
}
private accountsList (): Array<KeyringJson> {
return Object
.values(accountsObservable.subject.getValue())
.map(({ json }) => json);
return transformAccounts(accountsObservable.subject.getValue());
}
// FIXME This looks very much like what we have in Tabs
private accountsSubscribe (id: string, port: chrome.runtime.Port): boolean {
const cb = createSubscription(id, port);
const subscription = accountsObservable.subject.subscribe((accounts: SubjectInfo) =>
cb(transformAccounts(accounts))
);
port.onDisconnect.addListener(() => {
unsubscribe(id);
subscription.unsubscribe();
});
return true;
}
private authorizeApprove ({ id }: MessageAuthorizeApprove): boolean {
@@ -79,6 +98,21 @@ export default class Extension {
return this._state.allAuthRequests;
}
// FIXME This looks very much like what we have in accounts
private authorizeSubscribe (id: string, port: chrome.runtime.Port): boolean {
const cb = createSubscription(id, port);
const subscription = this._state.authSubject.subscribe((requests: Array<AuthorizeRequest>) =>
cb(requests)
);
port.onDisconnect.addListener(() => {
unsubscribe(id);
subscription.unsubscribe();
});
return true;
}
private seedCreate ({ length = SEED_DEFAULT_LENGTH, type }: MessageSeedCreate): MessageSeedCreate$Response {
const seed = mnemonicGenerate(length);
@@ -143,7 +177,22 @@ export default class Extension {
return this._state.allSignRequests;
}
async handle (type: MessageTypes, request: any): Promise<any> {
// FIXME This looks very much like what we have in authorization
private signingSubscribe (id: string, port: chrome.runtime.Port): boolean {
const cb = createSubscription(id, port);
const subscription = this._state.signSubject.subscribe((requests: Array<SigningRequest>) =>
cb(requests)
);
port.onDisconnect.addListener(() => {
unsubscribe(id);
subscription.unsubscribe();
});
return true;
}
async handle (id: string, type: MessageTypes, request: any, port: chrome.runtime.Port): Promise<any> {
switch (type) {
case 'authorize.approve':
return this.authorizeApprove(request);
@@ -154,6 +203,9 @@ export default class Extension {
case 'authorize.requests':
return this.authorizeRequests();
case 'authorize.subscribe':
return this.authorizeSubscribe(id, port);
case 'accounts.create':
return this.accountsCreate(request);
@@ -166,6 +218,9 @@ export default class Extension {
case 'accounts.list':
return this.accountsList();
case 'accounts.subscribe':
return this.accountsSubscribe(id, port);
case 'seed.create':
return this.seedCreate(request);
@@ -181,6 +236,9 @@ export default class Extension {
case 'signing.requests':
return this.signingRequests();
case 'signing.subscribe':
return this.signingSubscribe(id, port);
default:
throw new Error(`Unable to handle message of type ${type}`);
}
@@ -5,6 +5,7 @@
import { AuthorizeRequest, MessageAuthorize, MessageExtrinsicSign, MessageExtrinsicSign$Response, SigningRequest } from '../types';
import extension from 'extensionizer';
import { BehaviorSubject } from 'rxjs';
import { assert } from '@polkadot/util';
type AuthRequest = {
@@ -45,6 +46,8 @@ export default class State {
private _authUrls: AuthUrls = {};
private _authRequests: { [index: string]: AuthRequest } = {};
private _signRequests: { [index: string]: SignRequest } = {};
readonly authSubject: BehaviorSubject<Array<AuthorizeRequest>> = new BehaviorSubject([] as Array<AuthorizeRequest>);
readonly signSubject: BehaviorSubject<Array<SigningRequest>> = new BehaviorSubject([] as Array<SigningRequest>);
get hasAuthRequests (): boolean {
return this.numAuthRequests === 0;
@@ -88,7 +91,7 @@ export default class State {
};
delete this._authRequests[id];
this.updateIcon();
this.updateIconAuth();
fn(result);
};
@@ -97,7 +100,7 @@ export default class State {
private signComplete = (id: string, fn: Function) => {
return (result: MessageExtrinsicSign$Response | Error): void => {
delete this._signRequests[id];
this.updateIcon();
this.updateIconSign();
fn(result);
};
@@ -123,6 +126,16 @@ export default class State {
extension.browserAction.setBadgeText({ text });
}
private updateIconAuth (): void {
this.authSubject.next(this.allAuthRequests);
this.updateIcon();
}
private updateIconSign (): void {
this.signSubject.next(this.allSignRequests);
this.updateIcon();
}
async authorizeUrl (url: string, request: MessageAuthorize): Promise<boolean> {
const idStr = this.stripUrl(url);
@@ -144,7 +157,7 @@ export default class State {
url
};
this.updateIcon();
this.updateIconAuth();
});
}
@@ -177,7 +190,7 @@ export default class State {
url
};
this.updateIcon();
this.updateIconSign();
});
}
}
@@ -2,15 +2,15 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { Unsubcall } from '@polkadot/extension-dapp/types';
import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
import { MessageTypes, MessageAuthorize, MessageExtrinsicSign, MessageExtrinsicSign$Response } from '../types';
import keyring from '@polkadot/ui-keyring';
import accountsObservable from '@polkadot/ui-keyring/observable/accounts';
import { assert, isFunction } from '@polkadot/util';
import { assert } from '@polkadot/util';
import State from './State';
import { createSubscription, unsubscribe } from './subscriptions';
type Accounts = Array<{ address: string, name?: string }>;
@@ -35,18 +35,19 @@ export default class Tabs {
return transformAccounts(accountsObservable.subject.getValue());
}
private accountsSubscribe (url: string, cb?: (accounts: Accounts) => void): Unsubcall {
if (!isFunction(cb)) {
throw new Error('Expected accountsSubscribe to be passed a subscriber');
}
// FIXME This looks very much like what we have in Extension
private accountsSubscribe (url: string, id: string, port: chrome.runtime.Port): boolean {
const cb = createSubscription(id, port);
const subscription = accountsObservable.subject.subscribe((accounts: SubjectInfo) =>
cb(transformAccounts(accounts))
);
return (): void => {
port.onDisconnect.addListener(() => {
unsubscribe(id);
subscription.unsubscribe();
};
});
return true;
}
private extrinsicSign (url: string, request: MessageExtrinsicSign): Promise<MessageExtrinsicSign$Response> {
@@ -58,7 +59,7 @@ export default class Tabs {
return this._state.signQueue(url, request);
}
async handle (type: MessageTypes, request: any, url: string, subscriber?: (data: any) => void): Promise<any> {
async handle (id: string, type: MessageTypes, request: any, url: string, port: chrome.runtime.Port): Promise<any> {
switch (type) {
case 'authorize.tab':
return this.authorize(url, request);
@@ -67,7 +68,7 @@ export default class Tabs {
return this.accountsList(url);
case 'accounts.subscribe':
return this.accountsSubscribe(url, subscriber);
return this.accountsSubscribe(url, id, port);
case 'extrinsic.sign':
return this.extrinsicSign(url, request);
@@ -15,13 +15,6 @@ const state = new State();
const extension = new Extension(state);
const tabs = new Tabs(state);
function createSubscription (id: string, port: chrome.runtime.Port): (data: any) => void {
// FIXME We are not handling actual unsubscribes yet, this is an issue
return (subscription: any) => {
port.postMessage({ id, subscription });
};
}
export default function handler ({ id, message, request }: MessageRequest, port: chrome.runtime.Port): void {
const isPopup = port.name === PORT_POPUP;
const sender = port.sender as chrome.runtime.MessageSender;
@@ -32,27 +25,15 @@ export default function handler ({ id, message, request }: MessageRequest, port:
console.log(` [in] ${source}`); // :: ${JSON.stringify(request)}`);
// This is not great - basically, based on the name (since there is only 1 atm),
// we create a subscription handler. Basically these handlers will continute stream
// results as they become available
const subscription = message.indexOf('.subscribe') !== -1
? createSubscription(id, port)
: undefined;
const promise = isPopup
? extension.handle(message, request)
: tabs.handle(message, request, from || FALLBACK_URL, subscription);
? extension.handle(id, message, request, port)
: tabs.handle(id, message, request, from || FALLBACK_URL, port);
promise
.then((response) => {
console.log(`[out] ${source}`); // :: ${JSON.stringify(response)}`);
if (subscription) {
// TODO See unsub handling above, here we need to store the
// actual unsubscribe and handle appropriately
port.postMessage({ id, response: true });
} else {
port.postMessage({ id, response });
}
port.postMessage({ id, response });
})
.catch((error) => {
console.log(`[err] ${source}:: ${error.message}`);
@@ -0,0 +1,31 @@
// Copyright 2019 @polkadot/extension authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
type Subscriptions = {
[index: string]: chrome.runtime.Port
};
const subscriptions: Subscriptions = {};
// return a subscription callback, that will send the data to the caller via the port
export function createSubscription (id: string, port: chrome.runtime.Port): (data: any) => void {
subscriptions[id] = port;
return (subscription: any) => {
if (subscriptions[id]) {
port.postMessage({ id, subscription });
}
};
}
// clear a previous subscriber
export function unsubscribe (id: string): void {
if (subscriptions[id]) {
console.log(`Unsubscribing from ${id}`);
delete subscriptions[id];
} else {
console.error(`Unable to unsubscribe from ${id}`);
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
import { KeypairType } from '@polkadot/util-crypto/types';
export type MessageTypes = 'authorize.approve' | 'authorize.reject' | 'authorize.requests' | 'authorize.tab' | 'accounts.create' | 'accounts.edit' | 'accounts.forget' | 'accounts.list' | 'accounts.subscribe' | 'extrinsic.sign' | 'seed.create' | 'seed.validate' | 'signing.approve' | 'signing.cancel' | 'signing.requests';
export type MessageTypes = 'authorize.approve' | 'authorize.reject' | 'authorize.requests' | 'authorize.subscribe' | 'authorize.tab' | 'accounts.create' | 'accounts.edit' | 'accounts.forget' | 'accounts.list' | 'accounts.subscribe' | 'extrinsic.sign' | 'seed.create' | 'seed.validate' | 'signing.approve' | 'signing.cancel' | 'signing.requests' | 'signing.subscribe';
export type AuthorizeRequest = [string, MessageAuthorize, string];
+10 -9
View File
@@ -14,12 +14,14 @@ import Injected from './Injected';
// - this injector, listens on the events, maps it to the original
// - resolves/rejects the promise with the result (or sub data)
type Handler = {
resolve: (data: any) => void,
reject: (error: Error) => void,
subscriber?: (data: any) => void
};
type Handlers = {
[index: string]: {
resolve: (data: any) => void,
reject: (error: Error) => void,
subscriber?: (data: any) => void
}
[index: string]: Handler
};
// small helper with the typescript types, just cast window
@@ -62,12 +64,11 @@ window.addEventListener('message', ({ data, source }) => {
if (!handler.subscriber) {
delete handlers[data.id];
} else if (data.subscription) {
handler.subscriber(data.subscription);
return;
}
if (data.error) {
if (data.subscription) {
(handler.subscriber as Function)(data.subscription);
} else if (data.error) {
handler.reject(new Error(data.error));
} else {
handler.resolve(data.response);