From cbffc37dcf68a60c1568bd4ced61d4504157c0dd Mon Sep 17 00:00:00 2001 From: Jaco Date: Wed, 10 Nov 2021 07:58:50 +0100 Subject: [PATCH] Optimize PortableRegistry (#4192) * Optimize PortableRegistry * Additional * Adjust shortcut * Adjust params * Move decorateMethod * Ordering * Adjust * Exports * Renames --- packages/api/src/bundle.ts | 6 +- packages/api/src/promise/decorateMethod.ts | 108 +++++++++++ packages/api/src/promise/index.ts | 167 ++++-------------- packages/api/src/rx/decorateMethod.ts | 9 + packages/api/src/rx/index.ts | 70 ++++---- .../PortableRegistry/PortableRegistry.ts | 145 ++++++++------- 6 files changed, 269 insertions(+), 236 deletions(-) create mode 100644 packages/api/src/promise/decorateMethod.ts create mode 100644 packages/api/src/rx/decorateMethod.ts diff --git a/packages/api/src/bundle.ts b/packages/api/src/bundle.ts index 3731044461..0ce5882330 100644 --- a/packages/api/src/bundle.ts +++ b/packages/api/src/bundle.ts @@ -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'; diff --git a/packages/api/src/promise/decorateMethod.ts b/packages/api/src/promise/decorateMethod.ts new file mode 100644 index 0000000000..df5ddc2112 --- /dev/null +++ b/packages/api/src/promise/decorateMethod.ts @@ -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 { + reject: (value: Error) => Observable; + resolve: (value: T) => void; +} + +// a Promise completion tracker, wrapping an isComplete variable that ensures the promise only resolves once +export function promiseTracker (resolve: (value: T) => void, reject: (value: Error) => void): Tracker { + let isCompleted = false; + + return { + reject: (error: Error): Observable => { + 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 | undefined] { + let callback: Callback | 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; + } + + // 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>>> (method: M, args: unknown[]): Promise>> { + 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>>> (method: M, args: unknown[], resultCb: Callback): UnsubscribePromise { + return new Promise((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>>> (method: M, options?: DecorateMethodOptions): StorageEntryPromiseOverloads { + const needsCallback = !!(options && options.methodName && options.methodName.includes('subscribe')); + + return function (...args: unknown[]): Promise>> | UnsubscribePromise { + const [actualArgs, resultCb] = extractArgs(args, needsCallback); + + return resultCb + ? decorateSubscribe(method, actualArgs, resultCb) + : decorateCall((options?.overrideNoSub as M) || method, actualArgs); + } as StorageEntryPromiseOverloads; +} diff --git a/packages/api/src/promise/index.ts b/packages/api/src/promise/index.ts index 9aeea2a821..e676bbc4d6 100644 --- a/packages/api/src/promise/index.ts +++ b/packages/api/src/promise/index.ts @@ -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 { - reject: (value: Error) => Observable; - 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 | undefined] { - let callback: Callback | 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; - } - - // 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 (resolve: (value: T) => void, reject: (value: Error) => void): Tracker { - let isCompleted = false; - - return { - reject: (error: Error): Observable => { - 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: Method, actualArgs: unknown[]): Promise>> { - 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: Method, actualArgs: unknown[], resultCb: Callback): UnsubscribePromise { - return new Promise((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: Method, options?: DecorateMethodOptions): StorageEntryPromiseOverloads { - const needsCallback = options && options.methodName && options.methodName.includes('subscribe'); - - return function (...args: unknown[]): Promise>> | 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; #isReadyOrErrorPromise: Promise; + /** + * @description Creates an instance of the ApiPromise class + * @param options Options to create an instance. This can be either [[ApiOptions]] or + * an [[WsProvider]]. + * @example + *
+ * + * ```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 - *
- * - * ```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 */ diff --git a/packages/api/src/rx/decorateMethod.ts b/packages/api/src/rx/decorateMethod.ts new file mode 100644 index 0000000000..1736c50ac0 --- /dev/null +++ b/packages/api/src/rx/decorateMethod.ts @@ -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 > (method: M): M { + return method; +} diff --git a/packages/api/src/rx/index.ts b/packages/api/src/rx/index.ts index 349f6c6243..ca50bff50d 100644 --- a/packages/api/src/rx/index.ts +++ b/packages/api/src/rx/index.ts @@ -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: Method): Method { - return method; -} +import { decorateMethodRx } from './decorateMethod'; /** * # @polkadot/api/rx @@ -116,6 +112,37 @@ export function decorateMethod > (method: Metho export class ApiRx extends ApiBase<'rxjs'> { #isReadyRx: Observable; + /** + * @description Create an instance of the ApiRx class + * @param options Options to create an instance. Can be either [[ApiOptions]] or [[WsProvider]] + * @example + *
+ * + * ```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>( + // 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 - *
- * - * ```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>( - // 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 */ diff --git a/packages/types/src/metadata/PortableRegistry/PortableRegistry.ts b/packages/types/src/metadata/PortableRegistry/PortableRegistry.ts index 026336bf6c..2272ed2829 100644 --- a/packages/types/src/metadata/PortableRegistry/PortableRegistry.ts +++ b/packages/types/src/metadata/PortableRegistry/PortableRegistry.ts @@ -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 = {}; 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, names: Record, params: Record): void { @@ -264,11 +280,22 @@ function registerTypes (lookup: PortableRegistry, lookups: Record { - const dedup = removeDuplicateNames(lookup, types.map((t) => - extractName(types, t) - )); +function extractTypeInfo (lookup: PortableRegistry, portable: PortableType[]): [Record, Record, Record, Record] { + const nameInfo: [number, string, SiTypeParameter[]][] = []; + const types: Record = {}; + 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 = {}; const names: Record = {}; const params: Record = {}; @@ -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 { - const result: Record = {}; - - 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' }, 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') }