Optimize PortableRegistry (#4192)

* Optimize PortableRegistry

* Additional

* Adjust shortcut

* Adjust params

* Move decorateMethod

* Ordering

* Adjust

* Exports

* Renames
This commit is contained in:
Jaco
2021-11-10 08:58:50 +02:00
committed by GitHub
parent 74ee42ed66
commit cbffc37dcf
6 changed files with 269 additions and 236 deletions
+4 -2
View File
@@ -5,6 +5,8 @@ export { Keyring } from '@polkadot/keyring';
export { WsProvider, HttpProvider } from '@polkadot/rpc-provider';
export { packageInfo } from './packageInfo';
export { ApiPromise, decorateMethod as decorateMethodPromise } from './promise';
export { ApiPromise } from './promise';
export { decorateMethodPromise } from './promise/decorateMethod';
export { SubmittableResult } from './submittable';
export { ApiRx, decorateMethod as decorateMethodRx } from './rx';
export { ApiRx } from './rx';
export { decorateMethodRx } from './rx/decorateMethod';
+108
View File
@@ -0,0 +1,108 @@
// Copyright 2017-2021 @polkadot/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Observable } from 'rxjs';
import type { Callback, Codec } from '@polkadot/types/types';
import type { DecorateFn, DecorateMethodOptions, ObsInnerType, StorageEntryPromiseOverloads, UnsubscribePromise, VoidFn } from '../types';
import { catchError, EMPTY, Subscription, tap } from 'rxjs';
import { assert, isFunction } from '@polkadot/util';
interface Tracker<T> {
reject: (value: Error) => Observable<never>;
resolve: (value: T) => void;
}
// a Promise completion tracker, wrapping an isComplete variable that ensures the promise only resolves once
export function promiseTracker<T> (resolve: (value: T) => void, reject: (value: Error) => void): Tracker<T> {
let isCompleted = false;
return {
reject: (error: Error): Observable<never> => {
if (!isCompleted) {
isCompleted = true;
reject(error);
}
return EMPTY;
},
resolve: (value: T): void => {
if (!isCompleted) {
isCompleted = true;
resolve(value);
}
}
};
}
// extract the arguments and callback params from a value array possibly containing a callback
function extractArgs (args: unknown[], needsCallback: boolean): [unknown[], Callback<Codec> | undefined] {
let callback: Callback<Codec> | undefined;
const actualArgs = args.slice();
// If the last arg is a function, we pop it, put it into callback.
// actualArgs will then hold the actual arguments to be passed to `method`
if (args.length && isFunction(args[args.length - 1])) {
callback = actualArgs.pop() as Callback<Codec>;
}
// When we need a subscription, ensure that a valid callback is actually passed
assert(!needsCallback || isFunction(callback), 'Expected a callback to be passed with subscriptions');
return [actualArgs, callback];
}
// Decorate a call for a single-shot result - retrieve and then immediate unsubscribe
function decorateCall<M extends DecorateFn<ObsInnerType<ReturnType<M>>>> (method: M, args: unknown[]): Promise<ObsInnerType<ReturnType<M>>> {
return new Promise((resolve, reject): void => {
// single result tracker - either reject with Error or resolve with Codec result
const tracker = promiseTracker(resolve, reject);
// encoding errors reject immediately, any result unsubscribes and resolves
const subscription: Subscription = method(...args)
.pipe(
catchError((error: Error) => tracker.reject(error))
)
.subscribe((result): void => {
tracker.resolve(result);
setTimeout(() => subscription.unsubscribe(), 0);
});
});
}
// Decorate a subscription where we have a result callback specified
function decorateSubscribe<M extends DecorateFn<ObsInnerType<ReturnType<M>>>> (method: M, args: unknown[], resultCb: Callback<Codec>): UnsubscribePromise {
return new Promise<VoidFn>((resolve, reject): void => {
// either reject with error or resolve with unsubscribe callback
const tracker = promiseTracker(resolve, reject);
// errors reject immediately, the first result resolves with an unsubscribe promise, all results via callback
const subscription: Subscription = method(...args)
.pipe(
catchError((error: Error) => tracker.reject(error)),
tap(() => tracker.resolve(() => subscription.unsubscribe()))
)
.subscribe((result): void => {
// queue result (back of queue to clear current)
setTimeout(() => resultCb(result) as void, 0);
});
});
}
/**
* @description Decorate method for ApiPromise, where the results are converted to the Promise equivalent
*/
export function decorateMethodPromise<M extends DecorateFn<ObsInnerType<ReturnType<M>>>> (method: M, options?: DecorateMethodOptions): StorageEntryPromiseOverloads {
const needsCallback = !!(options && options.methodName && options.methodName.includes('subscribe'));
return function (...args: unknown[]): Promise<ObsInnerType<ReturnType<M>>> | UnsubscribePromise {
const [actualArgs, resultCb] = extractArgs(args, needsCallback);
return resultCb
? decorateSubscribe(method, actualArgs, resultCb)
: decorateCall((options?.overrideNoSub as M) || method, actualArgs);
} as StorageEntryPromiseOverloads;
}
+35 -132
View File
@@ -1,110 +1,13 @@
// Copyright 2017-2021 @polkadot/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Observable } from 'rxjs';
import type { Callback, Codec } from '@polkadot/types/types';
import type { ApiOptions, DecorateFn, DecorateMethodOptions, ObsInnerType, StorageEntryPromiseOverloads, UnsubscribePromise, VoidFn } from '../types';
import type { ApiOptions, UnsubscribePromise } from '../types';
import { catchError, EMPTY, Subscription, tap } from 'rxjs';
import { assert, isFunction, objectSpread } from '@polkadot/util';
import { objectSpread } from '@polkadot/util';
import { ApiBase } from '../base';
import { Combinator, CombinatorCallback, CombinatorFunction } from './Combinator';
interface Tracker<T> {
reject: (value: Error) => Observable<never>;
resolve: (value: T) => void;
}
// extract the arguments and callback params from a value array possibly containing a callback
function extractArgs (args: unknown[], needsCallback: boolean): [unknown[], Callback<Codec> | undefined] {
let callback: Callback<Codec> | undefined;
const actualArgs = args.slice();
// If the last arg is a function, we pop it, put it into callback.
// actualArgs will then hold the actual arguments to be passed to `method`
if (args.length && isFunction(args[args.length - 1])) {
callback = actualArgs.pop() as Callback<Codec>;
}
// When we need a subscription, ensure that a valid callback is actually passed
assert(!needsCallback || isFunction(callback), 'Expected a callback to be passed with subscriptions');
return [actualArgs, callback];
}
// a Promise completion tracker, wrapping an isComplete variable that ensures the promise only resolves once
function promiseTracker<T> (resolve: (value: T) => void, reject: (value: Error) => void): Tracker<T> {
let isCompleted = false;
return {
reject: (error: Error): Observable<never> => {
if (!isCompleted) {
isCompleted = true;
reject(error);
}
return EMPTY;
},
resolve: (value: T): void => {
if (!isCompleted) {
isCompleted = true;
resolve(value);
}
}
};
}
// Decorate a call for a single-shot result - retrieve and then immediate unsubscribe
function decorateCall<Method extends DecorateFn<ObsInnerType<ReturnType<Method>>>> (method: Method, actualArgs: unknown[]): Promise<ObsInnerType<ReturnType<Method>>> {
return new Promise((resolve, reject): void => {
// single result tracker - either reject with Error or resolve with Codec result
const tracker = promiseTracker(resolve, reject);
// encoding errors reject immediately, any result unsubscribes and resolves
const subscription: Subscription = method(...actualArgs).pipe(
catchError((error: Error) => tracker.reject(error))
).subscribe((result): void => {
tracker.resolve(result);
setTimeout(() => subscription.unsubscribe(), 0);
});
});
}
// Decorate a subscription where we have a result callback specified
function decorateSubscribe<Method extends DecorateFn<ObsInnerType<ReturnType<Method>>>> (method: Method, actualArgs: unknown[], resultCb: Callback<Codec>): UnsubscribePromise {
return new Promise<VoidFn>((resolve, reject): void => {
// either reject with error or resolve with unsubscribe callback
const tracker = promiseTracker(resolve, reject);
// errors reject immediately, the first result resolves with an unsubscribe promise, all results via callback
const subscription: Subscription = method(...actualArgs).pipe(
catchError((error: Error) => tracker.reject(error)),
tap(() => tracker.resolve(() => subscription.unsubscribe()))
).subscribe((result): void => {
// queue result (back of queue to clear current)
setTimeout(() => resultCb(result) as void, 0);
});
});
}
/**
* @description Decorate method for ApiPromise, where the results are converted to the Promise equivalent
*/
export function decorateMethod<Method extends DecorateFn<ObsInnerType<ReturnType<Method>>>> (method: Method, options?: DecorateMethodOptions): StorageEntryPromiseOverloads {
const needsCallback = options && options.methodName && options.methodName.includes('subscribe');
return function (...args: unknown[]): Promise<ObsInnerType<ReturnType<Method>>> | UnsubscribePromise {
const [actualArgs, resultCb] = extractArgs(args, !!needsCallback);
return resultCb
? decorateSubscribe(method, actualArgs, resultCb)
: decorateCall((options?.overrideNoSub as Method) || method, actualArgs);
} as StorageEntryPromiseOverloads;
}
import { decorateMethodPromise, promiseTracker } from './decorateMethod';
/**
* # @polkadot/api/promise
@@ -194,6 +97,38 @@ export class ApiPromise extends ApiBase<'promise'> {
#isReadyPromise: Promise<ApiPromise>;
#isReadyOrErrorPromise: Promise<ApiPromise>;
/**
* @description Creates an instance of the ApiPromise class
* @param options Options to create an instance. This can be either [[ApiOptions]] or
* an [[WsProvider]].
* @example
* <BR>
*
* ```javascript
* import Api from '@polkadot/api/promise';
*
* new Api().isReady.then((api) => {
* api.rpc.subscribeNewHeads((header) => {
* console.log(`new block #${header.number.toNumber()}`);
* });
* });
* ```
*/
constructor (options?: ApiOptions) {
super(options, 'promise', decorateMethodPromise);
this.#isReadyPromise = new Promise((resolve): void => {
super.once('ready', () => resolve(this));
});
this.#isReadyOrErrorPromise = new Promise((resolve, reject): void => {
const tracker = promiseTracker(resolve, reject);
super.once('ready', () => tracker.resolve(this));
super.once('error', (error: Error) => tracker.reject(error));
});
}
/**
* @description Creates an ApiPromise instance using the supplied provider. Returns an Promise containing the actual Api instance.
* @param options options that is passed to the class contructor. Can be either [[ApiOptions]] or a
@@ -227,38 +162,6 @@ export class ApiPromise extends ApiBase<'promise'> {
return instance.isReady;
}
/**
* @description Creates an instance of the ApiPromise class
* @param options Options to create an instance. This can be either [[ApiOptions]] or
* an [[WsProvider]].
* @example
* <BR>
*
* ```javascript
* import Api from '@polkadot/api/promise';
*
* new Api().isReady.then((api) => {
* api.rpc.subscribeNewHeads((header) => {
* console.log(`new block #${header.number.toNumber()}`);
* });
* });
* ```
*/
constructor (options?: ApiOptions) {
super(options, 'promise', decorateMethod);
this.#isReadyPromise = new Promise((resolve): void => {
super.once('ready', () => resolve(this));
});
this.#isReadyOrErrorPromise = new Promise((resolve, reject): void => {
const tracker = promiseTracker(resolve, reject);
super.once('ready', () => tracker.resolve(this));
super.once('error', (error: Error) => tracker.reject(error));
});
}
/**
* @description Promise that resolves the first time we are connected and loaded
*/
+9
View File
@@ -0,0 +1,9 @@
// Copyright 2017-2021 @polkadot/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Codec } from '@polkadot/types/types';
import type { DecorateFn } from '../types';
export function decorateMethodRx <M extends DecorateFn<Codec>> (method: M): M {
return method;
}
+33 -37
View File
@@ -1,18 +1,14 @@
// Copyright 2017-2021 @polkadot/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Codec } from '@polkadot/types/types';
import type { ApiOptions, DecorateFn } from '../types';
import type { ApiOptions } from '../types';
import { from, Observable } from 'rxjs';
import { objectSpread } from '@polkadot/util';
import { ApiBase } from '../base';
export function decorateMethod <Method extends DecorateFn<Codec>> (method: Method): Method {
return method;
}
import { decorateMethodRx } from './decorateMethod';
/**
* # @polkadot/api/rx
@@ -116,6 +112,37 @@ export function decorateMethod <Method extends DecorateFn<Codec>> (method: Metho
export class ApiRx extends ApiBase<'rxjs'> {
#isReadyRx: Observable<ApiRx>;
/**
* @description Create an instance of the ApiRx class
* @param options Options to create an instance. Can be either [[ApiOptions]] or [[WsProvider]]
* @example
* <BR>
*
* ```javascript
* import { switchMap } from 'rxjs';
* import Api from '@polkadot/api/rx';
*
* new Api().isReady
* .pipe(
* switchMap((api) =>
* api.rpc.chain.subscribeNewHeads()
* ))
* .subscribe((header) => {
* console.log(`new block #${header.number.toNumber()}`);
* });
* ```
*/
constructor (options?: ApiOptions) {
super(options, 'rxjs', decorateMethodRx);
this.#isReadyRx = from<Promise<ApiRx>>(
// You can create an observable from an event, however my mind groks this form better
new Promise((resolve): void => {
super.on('ready', () => resolve(this));
})
);
}
/**
* @description Creates an ApiRx instance using the supplied provider. Returns an Observable containing the actual Api instance.
* @param options options that is passed to the class constructor. Can be either [[ApiOptions]] or [[WsProvider]]
@@ -140,37 +167,6 @@ export class ApiRx extends ApiBase<'rxjs'> {
return new ApiRx(options).isReady;
}
/**
* @description Create an instance of the ApiRx class
* @param options Options to create an instance. Can be either [[ApiOptions]] or [[WsProvider]]
* @example
* <BR>
*
* ```javascript
* import { switchMap } from 'rxjs';
* import Api from '@polkadot/api/rx';
*
* new Api().isReady
* .pipe(
* switchMap((api) =>
* api.rpc.chain.subscribeNewHeads()
* ))
* .subscribe((header) => {
* console.log(`new block #${header.number.toNumber()}`);
* });
* ```
*/
constructor (options?: ApiOptions) {
super(options, 'rxjs', decorateMethod);
this.#isReadyRx = from<Promise<ApiRx>>(
// You can create an observable from an event, however my mind groks this form better
new Promise((resolve): void => {
super.on('ready', () => resolve(this));
})
);
}
/**
* @description Observable that returns the first time we are connected and loaded
*/
@@ -57,6 +57,9 @@ const WRAPPERS = ['BoundedBTreeMap', 'BoundedVec', 'Box', 'BTreeMap', 'Cow', 'Re
// These are reserved and/or conflicts with built-in Codec or JS definitions
const RESERVED = ['entries', 'hash', 'keys', 'new', 'size'];
// Remove these from all paths at index 1
const PATH_RM_INDEX_1 = ['generic', 'misc', 'pallet', 'traits', 'types'];
function splitNamespace (values: string[]): string[][] {
return values.map((v) => v.split('::'));
}
@@ -103,17 +106,34 @@ function getAliasPath (path: SiPath): string | null {
: null;
}
function hasNoDupes (input: [number, string, SiTypeParameter[]][]): boolean {
for (let i = 0; i < input.length; i++) {
const [ai, an] = input[i];
for (let j = 0; j < input.length; j++) {
const [bi, bn] = input[j];
// if the indexes are not the same and the names match, we have a dupe
if (ai !== bi && an === bn) {
return false;
}
}
}
return true;
}
function removeDuplicateNames (lookup: PortableRegistry, names: [number, string | null, SiTypeParameter[]][]): [number, string, SiTypeParameter[]][] {
const rewrite: Record<number, string> = {};
return names
.map(([lookupIndex, name, params]): [number, string | null, SiTypeParameter[]] => {
.map(([lookupIndex, name, params]): [number, string, SiTypeParameter[]] | null => {
if (!name) {
return [lookupIndex, null, params];
return null;
}
// those where the name is matching
const allSame = names.filter(([, oName]) => name === oName);
// those where the name is matching (since name is filtered, these all do have names)
const allSame = names.filter(([, oName]) => name === oName) as [number, string, SiTypeParameter[]][];
// are there among matching names
const anyDiff = allSame.some(([oIndex,, oParams]) =>
@@ -146,43 +166,38 @@ function removeDuplicateNames (lookup: PortableRegistry, names: [number, string
}
// see if using the param type helps
const adjusted = allSame.map(([oIndex, oName, oParams]): [number, string | null, SiTypeParameter[]] => {
const adjusted = new Array<[number, string, SiTypeParameter[]]>(allSame.length);
for (let i = 0; i < allSame.length; i++) {
const [oIndex, oName, oParams] = allSame[i];
const { def, path } = lookup.getSiType(oParams[paramIdx].type.unwrap());
if (!def.isPrimitive && !path.length) {
return [oIndex, null, params];
return null;
}
return [
adjusted[i] = [
oIndex,
def.isPrimitive
? `${oName as string}${def.asPrimitive.toString()}`
: `${oName as string}${path[path.length - 1].toString()}`,
? `${oName}${def.asPrimitive.toString()}`
: `${oName}${path[path.length - 1].toString()}`,
params
];
});
// any dupes remaining?
const noDupes = adjusted.every(([i, n]) =>
!!n &&
!adjusted.some(([ai, an]) =>
i !== ai &&
n === an
)
);
if (noDupes) {
// we filtered above for null names
adjusted.forEach(([index, name]): void => {
rewrite[index] = name as string;
});
}
return noDupes
? [lookupIndex, name, params]
: [lookupIndex, null, params];
if (hasNoDupes(adjusted)) {
for (let i = 0; i < adjusted.length; i++) {
const [index, name] = adjusted[i];
rewrite[index] = name;
}
return [lookupIndex, name, params];
}
return null;
})
.filter((n): n is [number, string, SiTypeParameter[]] => !!n[1])
.filter((n): n is [number, string, SiTypeParameter[]] => !!n)
.map(([lookupIndex, name, params]) => [
lookupIndex,
rewrite[lookupIndex] || name,
@@ -190,32 +205,33 @@ function removeDuplicateNames (lookup: PortableRegistry, names: [number, string
]);
}
function extractName (types: PortableType[], { id, type: { params, path } }: PortableType): [number, string | null, SiTypeParameter[]] {
const lookupIndex = id.toNumber();
function extractName (types: PortableType[], { id, type: { params, path } }: PortableType): [number, string, SiTypeParameter[]] | null {
// if we have no path or determined as a wrapper, we just skip it
if (!path.length || WRAPPERS.includes(path[path.length - 1].toString())) {
return [lookupIndex, null, []];
return null;
}
const parts = path
.map((p) => stringUpperFirst(stringCamelCase(p)))
.filter((p, index) =>
(
.filter((p, index) => {
const lower = p.toLowerCase();
return (
// Remove ::{generic, misc, pallet, traits, types}::
index !== 1 ||
!['Generic', 'Misc', 'Pallet', 'Traits', 'Types'].includes(p.toString())
!PATH_RM_INDEX_1.includes(lower)
) &&
(
// sp_runtime::generic::digest::Digest -> sp_runtime::generic::Digest
// sp_runtime::multiaddress::MultiAddress -> sp_runtime::MultiAddress
index === path.length - 1 ||
p.toLowerCase() !== path[index + 1].toLowerCase()
)
);
lower !== path[index + 1].toLowerCase()
);
});
let typeName = parts.join('');
if (parts.length === 2 && parts[parts.length - 1] === 'RawOrigin' && params.length === 2 && params[1].type.isSome) {
// Do magic for RawOrigin lookup
// do magic for RawOrigin lookup, e.g. pallet_collective::RawOrigin
if (parts.length === 2 && parts[1] === 'RawOrigin' && params.length === 2 && params[1].type.isSome) {
const instanceType = types[params[1].type.unwrap().toNumber()];
if (instanceType.type.path.length === 2) {
@@ -223,7 +239,7 @@ function extractName (types: PortableType[], { id, type: { params, path } }: Por
}
}
return [lookupIndex, typeName, params];
return [id.toNumber(), typeName, params];
}
function registerTypes (lookup: PortableRegistry, lookups: Record<string, string>, names: Record<number, string>, params: Record<string, SiTypeParameter[]>): void {
@@ -264,11 +280,22 @@ function registerTypes (lookup: PortableRegistry, lookups: Record<string, string
}
}
function extractNames (lookup: PortableRegistry, types: PortableType[]): Record<number, string> {
const dedup = removeDuplicateNames(lookup, types.map((t) =>
extractName(types, t)
));
function extractTypeInfo (lookup: PortableRegistry, portable: PortableType[]): [Record<number, PortableType>, Record<string, string>, Record<number, string>, Record<string, SiTypeParameter[]>] {
const nameInfo: [number, string, SiTypeParameter[]][] = [];
const types: Record<number, PortableType> = {};
for (let i = 0; i < portable.length; i++) {
const type = portable[i];
const extracted = extractName(portable, portable[i]);
if (extracted) {
nameInfo.push(extracted);
}
types[type.id.toNumber()] = type;
}
const dedup = removeDuplicateNames(lookup, nameInfo);
const lookups: Record<string, string> = {};
const names: Record<number, string> = {};
const params: Record<string, SiTypeParameter[]> = {};
@@ -281,23 +308,7 @@ function extractNames (lookup: PortableRegistry, types: PortableType[]): Record<
params[name] = p;
}
registerTypes(lookup, lookups, names, params);
return names;
}
// types have an id, which means they are to be named by
// the specified id - ensure we have a mapping lookup for these
function extractTypeMap (types: PortableType[]): Record<number, PortableType> {
const result: Record<number, PortableType> = {};
for (let i = 0; i < types.length; i++) {
const p = types[i];
result[p.id.toNumber()] = p;
}
return result;
return [types, lookups, names, params];
}
export class PortableRegistry extends Struct {
@@ -312,8 +323,12 @@ export class PortableRegistry extends Struct {
types: 'Vec<PortableType>'
}, value);
this.#names = extractNames(this, this.types);
this.#types = extractTypeMap(this.types);
const [types, lookups, names, params] = extractTypeInfo(this, this.types);
this.#names = names;
this.#types = types;
registerTypes(this, lookups, names, params);
// console.timeEnd('PortableRegistry')
}