Subscriptions return unsubscribe/dispose function (#527)

* Combinator return unsubscribe/dispose function

* Submittable extrinsics return UnsubFunction

* RPC methods with unsub callback returns

* Update changelog, bump version

* CHANGELOG typo

* Fix combinator unsubscribes

* Be explicit in returns for UnsubFunction (combineLatest)
This commit is contained in:
Jaco Greeff
2019-01-03 16:31:09 +01:00
committed by GitHub
parent 15d75f7166
commit 3be56ad56c
19 changed files with 173 additions and 137 deletions
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@polkadot/api-observable",
"version": "0.35.19",
"version": "0.36.0",
"description": "An Observable wrapper around the Polkadot JS API",
"main": "index.js",
"keywords": [
@@ -30,10 +30,10 @@
"homepage": "https://github.com/polkadot-js/api/tree/master/packages/rpc-rx#readme",
"dependencies": {
"@babel/runtime": "^7.2.0",
"@polkadot/extrinsics": "^0.35.19",
"@polkadot/rpc-core": "^0.35.19",
"@polkadot/rpc-provider": "^0.35.19",
"@polkadot/storage": "^0.35.19",
"@polkadot/extrinsics": "^0.36.0",
"@polkadot/rpc-core": "^0.36.0",
"@polkadot/rpc-provider": "^0.36.0",
"@polkadot/storage": "^0.36.0",
"@types/rx": "^4.1.1",
"rxjs": "^6.3.3"
}
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@polkadot/api",
"version": "0.35.19",
"version": "0.36.0",
"description": "An RxJS wrapper around the Polkadot JS RPC",
"main": "index.js",
"keywords": [
@@ -30,11 +30,11 @@
"homepage": "https://github.com/polkadot-js/api/tree/master/packages/api#readme",
"dependencies": {
"@babel/runtime": "^7.2.0",
"@polkadot/extrinsics": "^0.35.19",
"@polkadot/rpc-provider": "^0.35.19",
"@polkadot/rpc-rx": "^0.35.19",
"@polkadot/storage": "^0.35.19",
"@polkadot/types": "^0.35.19",
"@polkadot/extrinsics": "^0.36.0",
"@polkadot/rpc-provider": "^0.36.0",
"@polkadot/rpc-rx": "^0.36.0",
"@polkadot/storage": "^0.36.0",
"@polkadot/types": "^0.36.0",
"@types/rx": "^4.1.1",
"rxjs": "^6.3.3"
}
+14 -43
View File
@@ -2,31 +2,27 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { UnsubFunction } from './types';
import { isFunction } from '@polkadot/util';
export type CombinatorCallback = (value: Array<any>) => any;
export type CombinatorFunction = {
(cb: (value: any) => any): Promise<any>,
unsubscribe?: (subscriptionsId: number) => Promise<any>
(cb: (value: any) => any): UnsubFunction | any
};
let combinatorId = 5000;
const allCombinators: { [index: number]: Combinator } = {};
export default class Combinator {
protected _allHasFired: boolean = false;
protected _callback: CombinatorCallback;
protected _fired: Array<boolean> = [];
protected _fns: Array<CombinatorFunction> = [];
protected _id: number = ++combinatorId;
protected _isActive: boolean = true;
protected _results: Array<any> = [];
protected _subscriptionIds: Array<Promise<number>> = [];
protected _unsubscriptions: Array<UnsubFunction | any> = [];
constructor (fns: Array<CombinatorFunction | [Array<any>, CombinatorFunction]>, callback: CombinatorCallback) {
allCombinators[this._id] = this;
this._callback = callback;
this._subscriptionIds = fns.map((input, index): Promise<number> => {
this._unsubscriptions = fns.map((input, index) => {
const [args, fn] = Array.isArray(input)
? input
: [[], input];
@@ -39,22 +35,6 @@ export default class Combinator {
});
}
static lookup (id: number): Combinator {
return allCombinators[id];
}
static unsubscribe (id: number): Promise<any> {
const combinator = Combinator.lookup(id);
return combinator
? combinator.unsubscribe()
: Promise.resolve(false);
}
get id (): number {
return this._id;
}
protected allHasFired (): boolean {
if (!this._allHasFired) {
this._allHasFired = this._fired.filter((hasFired) => !hasFired).length === 0;
@@ -73,7 +53,7 @@ export default class Combinator {
}
protected triggerUpdate (): void {
if (!isFunction(this._callback) || !this.allHasFired()) {
if (!this._isActive || !isFunction(this._callback) || !this.allHasFired()) {
return;
}
@@ -84,22 +64,13 @@ export default class Combinator {
}
}
unsubscribe (): Promise<any> {
delete allCombinators[this._id];
unsubscribe (): void {
this._isActive = false;
return Promise.all(
this._subscriptionIds.map((subscriptionPromise, index) => {
const unsubscribe = this._fns[index].unsubscribe;
return !unsubscribe
? Promise.resolve(true)
: subscriptionPromise
.then((subscriptionId) =>
unsubscribe(subscriptionId)
)
.then(() => true)
.catch(() => false);
})
);
this._unsubscriptions.forEach((unsubscribe) => {
if (isFunction(unsubscribe)) {
unsubscribe();
}
});
}
}
+2 -14
View File
@@ -80,12 +80,7 @@ describe('Combinator', () => {
});
it('unsubscribes as required', (done) => {
const mocker = () => Promise.resolve(12345);
mocker.unsubscribe = async (subId: number) => {
if (subId === 12345) {
done();
}
};
const mocker = () => done;
const combinator = new Combinator([
mocker,
() => Promise.resolve(98765)
@@ -93,13 +88,6 @@ describe('Combinator', () => {
// ignore
});
combinator
.unsubscribe()
.then(() => {
// ignore
})
.catch(() => {
// ignore
});
combinator.unsubscribe();
});
});
@@ -5,7 +5,7 @@
import { KeyringPair } from '@polkadot/keyring/types';
import { AnyNumber, AnyU8a } from '@polkadot/types/types';
import { SubmittableSendResult } from '../types';
import { ApiPromiseInterface } from './types';
import { ApiPromiseInterface, UnsubFunction } from './types';
import { EventRecord, Extrinsic, ExtrinsicStatus, Hash, Method, SignedBlock } from '@polkadot/types/index';
@@ -41,7 +41,7 @@ export default class SubmittableExtrinsic extends Extrinsic {
};
}
send (statusCb?: (result: SubmittableSendResult) => any): Promise<Hash> {
send (statusCb?: (result: SubmittableSendResult) => any): Promise<Hash> | UnsubFunction {
if (!statusCb || !this._api.hasSubscriptions) {
return this._api.rpc.author.submitExtrinsic(this);
}
+68 -16
View File
@@ -2,9 +2,10 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { RpcInterface$Section$Method } from '@polkadot/rpc-core/types';
import { ProviderInterface } from '@polkadot/rpc-provider/types';
import { ApiOptions } from '../types';
import { ApiPromiseInterface, QueryableStorageFunction, QueryableModuleStorage, QueryableStorage, SubmittableExtrinsics, SubmittableModuleExtrinsics, SubmittableExtrinsicFunction } from './types';
import { ApiPromiseInterface, DecoratedRpc, DecoratedRpc$Method, DecoratedRpc$Section, QueryableStorageFunction, QueryableModuleStorage, QueryableStorage, SubmittableExtrinsics, SubmittableModuleExtrinsics, SubmittableExtrinsicFunction, UnsubFunction } from './types';
import Rpc from '@polkadot/rpc-core/index';
import { Storage } from '@polkadot/storage/types';
@@ -12,12 +13,16 @@ import { Hash } from '@polkadot/types/index';
import { Codec } from '@polkadot/types/types';
import { MethodFunction, ModulesWithMethods } from '@polkadot/types/Method';
import { StorageFunction } from '@polkadot/types/StorageKey';
import { isFunction, logger } from '@polkadot/util';
import { isFunction, logger, assert } from '@polkadot/util';
import ApiBase from '../Base';
import Combinator, { CombinatorCallback, CombinatorFunction } from './Combinator';
import SubmittableExtrinsic from './SubmittableExtrinsic';
const NOOP = () => {
// ignore
};
const l = logger('api/promise');
/**
@@ -106,7 +111,7 @@ const l = logger('api/promise');
* });
* ```
*/
export default class ApiPromise extends ApiBase<Rpc, QueryableStorage, SubmittableExtrinsics> implements ApiPromiseInterface {
export default class ApiPromise extends ApiBase<DecoratedRpc, QueryableStorage, SubmittableExtrinsics> implements ApiPromiseInterface {
private _isReady: Promise<ApiPromise>;
/**
@@ -173,8 +178,50 @@ export default class ApiPromise extends ApiBase<Rpc, QueryableStorage, Submittab
return this._isReady;
}
protected decorateRpc (rpc: Rpc): Rpc {
return rpc;
protected decorateRpc (rpc: Rpc): DecoratedRpc {
const names = ['author', 'chain', 'state', 'system'] as Array<keyof DecoratedRpc>;
return names.reduce((result, section) => {
result[section] = Object.keys(rpc[section]).reduce((fns, method) => {
fns[method] = this.decorateMethod(rpc[section][method]);
return fns;
}, {} as DecoratedRpc$Section);
return result;
}, {} as DecoratedRpc);
}
protected decorateMethod (fn: RpcInterface$Section$Method): DecoratedRpc$Method {
if (!isFunction(fn.unsubscribe)) {
return fn;
}
return (..._params: Array<any>): UnsubFunction => {
const cb = _params[_params.length - 1];
assert(isFunction(cb), 'Expected callback as last paramater for subscription');
let isActive = true;
const params = _params
.slice(0, _params.length - 1)
.concat([(value?: any) => {
return !isActive
? undefined
: cb(value);
}]);
const subscriptionIdPromise = fn(...params);
return (): void => {
if (!isActive) {
return;
}
isActive = false;
subscriptionIdPromise
.then((subscriptionId) => fn.unsubscribe(subscriptionId))
.catch(NOOP);
};
};
}
/**
@@ -197,8 +244,12 @@ export default class ApiPromise extends ApiBase<Rpc, QueryableStorage, Submittab
* });
* ```
*/
combineLatest (fns: Array<CombinatorFunction>, callback: CombinatorCallback): Combinator {
return new Combinator(fns, callback);
combineLatest (fns: Array<CombinatorFunction>, callback: CombinatorCallback): UnsubFunction {
const combinator = new Combinator(fns, callback);
return (): void => {
combinator.unsubscribe();
};
}
protected decorateExtrinsics (extrinsics: ModulesWithMethods): SubmittableExtrinsics {
@@ -237,7 +288,13 @@ export default class ApiPromise extends ApiBase<Rpc, QueryableStorage, Submittab
}
private decorateStorageEntry (method: StorageFunction): QueryableStorageFunction {
const decorated: any = (...args: Array<any>): Promise<Codec | null | undefined> | Promise<number> => {
const subscribe = (...args: Array<any>) =>
this.rpc.state.subscribeStorage(
[[method, args.length === 1 ? undefined : args[0]]],
(result: Array<Codec | null | undefined> = []) =>
args[args.length - 1](result[0])
) as UnsubFunction;
const decorated: any = (...args: Array<any>): Promise<Codec | null | undefined> | UnsubFunction => {
if (args.length === 0 || !isFunction(args[args.length - 1])) {
return this.rpc.state.getStorage([method, args[0]]);
} else if (!this.hasSubscriptions && isFunction(args[args.length - 1])) {
@@ -246,17 +303,12 @@ export default class ApiPromise extends ApiBase<Rpc, QueryableStorage, Submittab
return this.rpc.state.getStorage([method, args.length === 1 ? undefined : args[0]]);
}
return this.rpc.state.subscribeStorage(
[[method, args.length === 1 ? undefined : args[0]]],
(result: Array<Codec | null | undefined> = []) =>
args[args.length - 1](result[0])
);
return subscribe;
};
decorated.at = (hash: Hash, arg?: any): Promise<Codec | null | undefined> =>
this.rpc.state.getStorage([method, arg], hash);
decorated.unsubscribe = (subscriptionId: number): Promise<any> =>
this.rpc.state.subscribeStorage.unsubscribe(subscriptionId);
this.rpc.state.getStorage([method, arg], hash) as Promise<Codec | null | undefined>;
decorated.subscribe = subscribe;
return this.decorateFunctionMeta(method, decorated) as QueryableStorageFunction;
}
+21 -6
View File
@@ -8,16 +8,31 @@ import { MethodFunction } from '@polkadot/types/Method';
import { StorageFunction } from '@polkadot/types/StorageKey';
import { ApiBaseInterface } from '../types';
import Rpc from '@polkadot/rpc-core/index';
import SubmittableExtrinsic from './SubmittableExtrinsic';
export type UnsubFunction = () => void;
export type DecoratedRpc$Method = (...params: Array<any>) => Promise<any> | UnsubFunction;
export interface DecoratedRpc$Section {
[index: string]: DecoratedRpc$Method;
}
export interface DecoratedRpc {
author: DecoratedRpc$Section;
chain: DecoratedRpc$Section;
state: DecoratedRpc$Section;
system: DecoratedRpc$Section;
}
export interface QueryableStorageFunction extends StorageFunction {
(cb: (value?: any | null) => any): UnsubFunction;
(arg: any, cb: (value?: any | null) => any): UnsubFunction;
(arg?: any): Promise<Codec | null | undefined>;
(cb: (value: any) => any): Promise<number>;
(arg: any, cb: (value: any) => any): Promise<number>;
at: (hash: Hash, arg?: any) => Promise<Codec | null | undefined>;
unsubscribe: (subscriptionId: number) => Promise<any>;
// This one is slightly more sane with regards to type-checking, requiring less boilerplate to
// coax the intent of the the actual function with a split between subs and calls
subscribe: ((arg: any, cb: (value?: any | null) => any) => UnsubFunction) | ((cb: (value?: any | null) => any) => UnsubFunction);
}
export interface QueryableModuleStorage {
@@ -40,7 +55,7 @@ export interface SubmittableExtrinsics {
[index: string]: SubmittableModuleExtrinsics;
}
export interface ApiPromiseInterface extends ApiBaseInterface<Rpc, QueryableStorage, SubmittableExtrinsics> {
export interface ApiPromiseInterface extends ApiBaseInterface<DecoratedRpc, QueryableStorage, SubmittableExtrinsics> {
readonly isReady: Promise<ApiPromiseInterface>;
}
+2 -1
View File
@@ -18,7 +18,8 @@ export interface ApiBaseInterface<R, S, E> {
readonly rpc: R;
readonly tx: E;
on: (type: ApiInterface$Events, handler: (...args: Array<any>) => any) => void;
on: (type: ApiInterface$Events, handler: (...args: Array<any>) => any) => this;
once: (type: ApiInterface$Events, handler: (...args: Array<any>) => any) => this;
}
export interface ApiOptions {
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@polkadot/rpc-core",
"version": "0.35.19",
"version": "0.36.0",
"description": "A JavaScript wrapper for the Polkadot JsonRPC interface",
"main": "index.js",
"keywords": [
@@ -30,9 +30,9 @@
"homepage": "https://github.com/polkadot-js/api/tree/master/packages/rpc-core#readme",
"dependencies": {
"@babel/runtime": "^7.2.0",
"@polkadot/jsonrpc": "^0.35.19",
"@polkadot/rpc-provider": "^0.35.19",
"@polkadot/types": "^0.35.19",
"@polkadot/util": "^0.33.27"
"@polkadot/jsonrpc": "^0.36.0",
"@polkadot/rpc-provider": "^0.36.0",
"@polkadot/types": "^0.36.0",
"@polkadot/util": "^0.33.28"
}
}
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@polkadot/rpc-provider",
"version": "0.35.19",
"version": "0.36.0",
"description": "Transport providers for the API",
"main": "index.js",
"keywords": [
@@ -30,10 +30,10 @@
"homepage": "https://github.com/polkadot-js/api/tree/master/packages/rpc-provider#readme",
"dependencies": {
"@babel/runtime": "^7.2.0",
"@polkadot/keyring": "^0.33.27",
"@polkadot/storage": "^0.35.19",
"@polkadot/util": "^0.33.27",
"@polkadot/util-crypto": "^0.33.27",
"@polkadot/keyring": "^0.33.28",
"@polkadot/storage": "^0.36.0",
"@polkadot/util": "^0.33.28",
"@polkadot/util-crypto": "^0.33.28",
"@types/nock": "^9.3.0",
"eventemitter3": "^3.1.0",
"isomorphic-fetch": "^2.2.1",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@polkadot/rpc-rx",
"version": "0.35.19",
"version": "0.36.0",
"description": "An RxJs wrapper around the Polkadot JS API",
"main": "index.js",
"keywords": [
@@ -30,8 +30,8 @@
"homepage": "https://github.com/polkadot-js/api/tree/master/packages/rpc-rx#readme",
"dependencies": {
"@babel/runtime": "^7.2.0",
"@polkadot/rpc-core": "^0.35.19",
"@polkadot/rpc-provider": "^0.35.19",
"@polkadot/rpc-core": "^0.36.0",
"@polkadot/rpc-provider": "^0.36.0",
"@types/rx": "^4.1.1",
"rxjs": "^6.3.3"
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@polkadot/extrinsics",
"version": "0.35.19",
"version": "0.36.0",
"main": "index.js",
"repository": "https://github.com/polkadot-js/api.git",
"author": "Jaco Greeff <jacogr@gmail.com>",
@@ -11,7 +11,7 @@
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^7.2.0",
"@polkadot/types": "^0.35.19",
"@polkadot/util": "^0.33.27"
"@polkadot/types": "^0.36.0",
"@polkadot/util": "^0.33.28"
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@polkadot/jsonrpc",
"version": "0.35.19",
"version": "0.36.0",
"description": "Method definitions for the Polkadot RPC layer",
"main": "index.js",
"engines": {
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@polkadot/storage",
"version": "0.35.19",
"version": "0.36.0",
"description": "Generic wrapper wrapper around state storage",
"main": "index.js",
"engines": {
@@ -30,9 +30,9 @@
"homepage": "https://github.com/polkadot-js/api/tree/master/packages/type-storage#readme",
"dependencies": {
"@babel/runtime": "^7.2.0",
"@polkadot/keyring": "^0.33.27",
"@polkadot/types": "^0.35.19",
"@polkadot/util": "^0.33.27",
"@polkadot/util-crypto": "^0.33.27"
"@polkadot/keyring": "^0.33.28",
"@polkadot/types": "^0.36.0",
"@polkadot/util": "^0.33.28",
"@polkadot/util-crypto": "^0.33.28"
}
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@polkadot/types",
"version": "0.35.19",
"version": "0.36.0",
"description": "Implementation of the Parity codec",
"main": "index.js",
"keywords": [
@@ -30,8 +30,8 @@
"homepage": "https://github.com/polkadot-js/api/tree/master/packages/types#readme",
"dependencies": {
"@babel/runtime": "^7.2.0",
"@polkadot/keyring": "^0.33.27",
"@polkadot/util": "^0.33.27",
"@polkadot/keyring": "^0.33.28",
"@polkadot/util": "^0.33.28",
"core-js": "^2.6.1"
}
}
+2 -3
View File
@@ -63,13 +63,12 @@ function addRpc () {
}).join(', ');
const type = '`' + method.type + '`';
const isSub = method.isSubscription;
const renderMethod = `${md}\n▸ **${methodName}**(${args})`;
const renderMethod = `${md}\n▸ **${methodName}**(${args}`;
const renderReturnType = `: ${type}`;
const renderSignature = `${renderMethod}${renderReturnType}`;
const renderSignatureSub = `${renderMethod}**.subscribe**(CALLBACK)${renderReturnType}`;
const renderSummary = `${method && method.description ? `\n- **summary**: ${method.description}\n` : `\n\n`}`;
return isSub ? `${renderSignatureSub}${renderSummary}` : `${renderSignature}${renderSummary}`;
return `${renderSignature}${isSub ? ', CALLBACK' : ''})${renderSummary}`;
}, renderSection);
}, renderHeading + renderAnchors);
}