From 00bc441f0efaefdf21c05bd6fd2560eb46f318fd Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Fri, 4 Sep 2020 17:57:02 +0200 Subject: [PATCH] Add connected() to API interface (#2580) * Add connected() to API interface * Slight doc update * Align ApiRx * isConnected() usage * Updates * Retrieve isConnected for API via observable * Remove (now) useless Rx test --- CHANGELOG.md | 22 ++++++++ docs/start/create.md | 4 +- package.json | 2 +- packages/api/src/base/Decorate.ts | 2 +- packages/api/src/base/Init.ts | 2 +- packages/api/src/base/index.ts | 18 +++++- packages/api/src/rx/Api.spec.ts | 9 --- packages/api/src/rx/Api.ts | 7 --- packages/rpc-core/src/index.ts | 18 +++++- packages/rpc-provider/src/http/index.spec.ts | 2 +- packages/rpc-provider/src/http/index.ts | 11 +++- packages/rpc-provider/src/mock/index.ts | 8 ++- packages/rpc-provider/src/types.ts | 6 +- packages/rpc-provider/src/ws/Provider.ts | 58 +++++++++++--------- packages/rpc-provider/src/ws/connect.spec.ts | 16 +++--- packages/rpc-provider/src/ws/index.spec.ts | 2 +- yarn.lock | 10 ++-- 17 files changed, 125 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f03888002..521867ab25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,31 @@ ## 1.32.0-beta.x +Upgrade priority: Low. However with the changes to connect/disconnect behavior, if using this to tracks state mnually inside your application an upgrade is recommended. + +- **Breaking change** Previously `isReady` could throw and error, now it will always succeed on connection. For trapping errors, use the `.isReadyOrError` variant on the API +- **Breaking change** The `isConnected` provider interface is now a getter, previous calls to `provider.isConnected()` should now be done via `provider.isConnected`. Likewise the `provider.disconnect()` is now async returning a Promises to complete alignment of intent. + +Contributed: + +- Expand vesting information via derive balances (Thanks to https://github.com/niklabh) +- Add `isReadyOrError` to API, `isReady` always succeeds (Thanks to https://github.com/shawntabrizi) + Changes: +- Adjust memoization to work on a per-instance basis, instance shared between API and Provider instances - Added `derive.chain.getBlock(hash)` to retrieve a `SignedBlock` extended with an `.author` (same as `derive.chain.getHeader(...)`) +- Added `api.{connect, disconnect}()` as well as `isConnected` interfaces. The first functions returning `Promise` +- Error on provider connections will now emit and error (as expected) via the event emitter +- Add `api.derive.accounts.accountId` to perform AccountId lookups (from indices or actual AccountId) +- Lessen load of `paymentInfo` queries to only use accounId mappings as available +- Adjust staking derives to cater for early Substrate 2.0 chains (optional/non-optional EraIndexes) +- Cater for the handling of nested aliased types, e.g. wrapped inside Vec or Tuple +- Add the support for the `grandpa_subscribeJustifications` RPC +- Adjust `Call.toHuman()` to remove more technical internal details +- static metadata & tests updated for the latest substrate master +- `toHuman()` & `.toBigInt()` has been explicitly added to the API documentation +- Adjust known types for latest Kusama network state ## 1.31.1 Aug 31, 2020 diff --git a/docs/start/create.md b/docs/start/create.md index b828b10b8d..51793b2838 100644 --- a/docs/start/create.md +++ b/docs/start/create.md @@ -49,7 +49,7 @@ The API creation is done via the `ApiPromise.create` interface which is a shortc ```js ApiPromise - .create({ provider: wsProvider }) + .create({ provider: wsProvider }).isReady .then((api) => console.log(api.genesisHash.toHex()) ); @@ -70,7 +70,7 @@ console.log(api.genesisHash.toHex()); ## Failures -In all cases the API will handle reconnections automatically. This means that when you connect and the endpoint is not (yet) ready, the promise will not resolve immediately, but rather when connected. The same applies to when connection is lost, the API will manage re-connections. +In all cases the API will handle reconnecting automatically. This means that when you connect and the endpoint is not (yet) ready, the promise will not resolve immediately, but rather when connected. The same applies to when connection is lost, the API will manage re-connections. In cases where the API does not support the chain being connected to, such as it using an unknown metadata version, the ready promise will fail to resolve and instead reject. diff --git a/package.json b/package.json index 0f4ce427b0..48610f4aa0 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "@babel/register": "^7.11.5", "@babel/runtime": "^7.11.2", "@polkadot/dev": "^0.55.45", - "@polkadot/ts": "^0.3.40", + "@polkadot/ts": "^0.3.41", "@polkadot/typegen": "workspace:packages/typegen", "@types/jest": "^26.0.13", "@vuepress/plugin-search": "^1.5.4", diff --git a/packages/api/src/base/Decorate.ts b/packages/api/src/base/Decorate.ts index 1f776ee2f5..de510ae55a 100644 --- a/packages/api/src/base/Decorate.ts +++ b/packages/api/src/base/Decorate.ts @@ -142,7 +142,7 @@ export default abstract class Decorate extends Events this._options = options; this._type = type; this._rpcCore = new RpcCore(this.#instanceId, this.#registry, thisProvider, this._options.rpc); - this._isConnected = new BehaviorSubject(this._rpcCore.provider.isConnected()); + this._isConnected = new BehaviorSubject(this._rpcCore.provider.isConnected); this._rx.hasSubscriptions = this._rpcCore.provider.hasSubscriptions; this._rx.registry = this.#registry; } diff --git a/packages/api/src/base/Init.ts b/packages/api/src/base/Init.ts index 2741d46370..49a18a4f9c 100644 --- a/packages/api/src/base/Init.ts +++ b/packages/api/src/base/Init.ts @@ -62,7 +62,7 @@ export default abstract class Init extends Decorate extends Init extends Init { + return this._rpcCore.connect(); + } + /** * @description Disconnect from the underlying provider, halting all network traffic */ - public disconnect (): void { - this._rpcCore.disconnect(); + public disconnect (): Promise { + return this._rpcCore.disconnect(); } /** diff --git a/packages/api/src/rx/Api.spec.ts b/packages/api/src/rx/Api.spec.ts index 69d773d874..3f858e6009 100644 --- a/packages/api/src/rx/Api.spec.ts +++ b/packages/api/src/rx/Api.spec.ts @@ -9,7 +9,6 @@ import { map, switchMap } from 'rxjs/operators'; import testingPairs from '@polkadot/keyring/testingPairs'; import Mock from '@polkadot/rpc-provider/mock/index'; import { TypeRegistry } from '@polkadot/types'; -import { isObservable } from '@polkadot/util'; import { SingleAccountSigner } from '../../test/util'; import ApiRx from './Api'; @@ -24,14 +23,6 @@ describe('ApiRx', (): void => { provider = new Mock(registry); }); - it('has isConnected', (): void => { - const api = new ApiRx(); - - expect( - isObservable(api.isConnected) - ).toBe(true); - }); - describe('decorator.signAsync', (): void => { it('signs a transfer using an external signer', (): void => { const signer = new SingleAccountSigner(registry, keyring.alice_session); diff --git a/packages/api/src/rx/Api.ts b/packages/api/src/rx/Api.ts index 0b97492242..dd012e0496 100644 --- a/packages/api/src/rx/Api.ts +++ b/packages/api/src/rx/Api.ts @@ -173,13 +173,6 @@ export default class ApiRx extends ApiBase<'rxjs'> { ); } - /** - * @description Observable that carries the connected state for the provider. Results in a boolean flag that is true/false based on the connectivity. - */ - public get isConnected (): Observable { - return this._isConnected; - } - /** * @description Observable that returns the first time we are connected and loaded */ diff --git a/packages/rpc-core/src/index.ts b/packages/rpc-core/src/index.ts index ade4f18297..ab1169568c 100644 --- a/packages/rpc-core/src/index.ts +++ b/packages/rpc-core/src/index.ts @@ -129,11 +129,25 @@ export default class Rpc implements RpcInterface { this.addUserInterfaces(userRpc); } + /** + * @description Returns the connected status of a provider + */ + public get isConnected (): boolean { + return this.provider.isConnected; + } + + /** + * @description Manually connect from the attached provider + */ + public connect (): Promise { + return this.provider.connect(); + } + /** * @description Manually disconnect from the attached provider */ - public disconnect (): void { - this.provider.disconnect(); + public disconnect (): Promise { + return this.provider.disconnect(); } /** diff --git a/packages/rpc-provider/src/http/index.spec.ts b/packages/rpc-provider/src/http/index.spec.ts index 7fe6f3a7b1..240b44b6ad 100644 --- a/packages/rpc-provider/src/http/index.spec.ts +++ b/packages/rpc-provider/src/http/index.spec.ts @@ -31,7 +31,7 @@ describe('Http', (): void => { }); it('always returns isConnected true', (): void => { - expect(http.isConnected()).toEqual(true); + expect(http.isConnected).toEqual(true); }); it('does not (yet) support subscribe', (): Promise => { diff --git a/packages/rpc-provider/src/http/index.ts b/packages/rpc-provider/src/http/index.ts index 1ff5296218..33d87826dc 100644 --- a/packages/rpc-provider/src/http/index.ts +++ b/packages/rpc-provider/src/http/index.ts @@ -67,10 +67,17 @@ export default class HttpProvider implements ProviderInterface { throw new Error('Unimplemented'); } + /** + * @description Manually connect from the connection + */ + public async connect (): Promise { + // noop + } + /** * @description Manually disconnect from the connection */ - public disconnect (): void { + public async disconnect (): Promise { // noop } @@ -78,7 +85,7 @@ export default class HttpProvider implements ProviderInterface { * @summary Whether the node is connected or not. * @return {boolean} true if connected */ - public isConnected (): boolean { + public get isConnected (): boolean { return true; } diff --git a/packages/rpc-provider/src/mock/index.ts b/packages/rpc-provider/src/mock/index.ts index b6c0392629..2083b99810 100644 --- a/packages/rpc-provider/src/mock/index.ts +++ b/packages/rpc-provider/src/mock/index.ts @@ -98,11 +98,15 @@ export default class Mock implements ProviderInterface { throw new Error('Unimplemented'); } - public disconnect (): void { + public async connect (): Promise { // noop } - public isConnected (): boolean { + public async disconnect (): Promise { + // noop + } + + public get isConnected (): boolean { return true; } diff --git a/packages/rpc-provider/src/types.ts b/packages/rpc-provider/src/types.ts index 10b444cc2b..9f40c807fa 100644 --- a/packages/rpc-provider/src/types.ts +++ b/packages/rpc-provider/src/types.ts @@ -44,9 +44,11 @@ export type ProviderInterfaceEmitCb = (value?: any) => any; export interface ProviderInterface { readonly hasSubscriptions: boolean; + readonly isConnected: boolean; + clone (): ProviderInterface; - disconnect (): void; - isConnected (): boolean; + connect (): Promise; + disconnect (): Promise; on (type: ProviderInterfaceEmitted, sub: ProviderInterfaceEmitCb): () => void; send (method: string, params: any[]): Promise; subscribe (type: string, method: string, params: any[], cb: ProviderInterfaceCallback): Promise; diff --git a/packages/rpc-provider/src/ws/Provider.ts b/packages/rpc-provider/src/ws/Provider.ts index c2156e92b6..3a384ad614 100644 --- a/packages/rpc-provider/src/ws/Provider.ts +++ b/packages/rpc-provider/src/ws/Provider.ts @@ -30,10 +30,6 @@ interface WsStateSubscription extends SubscriptionHandler { params: any[]; } -interface WSProviderInterface extends ProviderInterface { - connect (): void; -} - const ALIASSES: { [index: string]: string } = { chain_finalisedHead: 'chain_finalizedHead', chain_subscribeFinalisedHeads: 'chain_subscribeFinalizedHeads', @@ -62,7 +58,7 @@ const l = logger('api-ws'); * * @see [[HttpProvider]] */ -export default class WsProvider implements WSProviderInterface { +export default class WsProvider implements ProviderInterface { readonly #coder: Coder; readonly #endpoints: string[]; @@ -123,6 +119,14 @@ export default class WsProvider implements WSProviderInterface { return true; } + /** + * @summary Whether the node is connected or not. + * @return {boolean} true if connected + */ + public get isConnected (): boolean { + return this.#isConnected; + } + /** * @description Returns a clone of the object */ @@ -152,31 +156,34 @@ export default class WsProvider implements WSProviderInterface { this.#websocket.onopen = this.#onSocketOpen; } catch (error) { l.error(error); + + this.#emit('error', error); + + throw error; } } /** * @description Manually disconnect from the connection, clearing autoconnect logic */ - public disconnect (): void { - if (isNull(this.#websocket)) { - throw new Error('Cannot disconnect on a non-open websocket'); + // eslint-disable-next-line @typescript-eslint/require-await + public async disconnect (): Promise { + try { + assert(!isNull(this.#websocket), 'Cannot disconnect on a non-connected websocket'); + + // switch off autoConnect, we are in manual mode now + this.#autoConnectMs = 0; + + // 1000 - Normal closure; the connection successfully completed + this.#websocket.close(1000); + this.#websocket = null; + } catch (error) { + l.error(error); + + this.#emit('error', error); + + throw error; } - - // switch off autoConnect, we are in manual mode now - this.#autoConnectMs = 0; - - // 1000 - Normal closure; the connection successfully completed - this.#websocket.close(1000); - this.#websocket = null; - } - - /** - * @summary Whether the node is connected or not. - * @return {boolean} true if connected - */ - public isConnected (): boolean { - return this.#isConnected; } /** @@ -220,7 +227,7 @@ export default class WsProvider implements WSProviderInterface { subscription }; - if (this.isConnected() && !isNull(this.#websocket)) { + if (this.isConnected && !isNull(this.#websocket)) { this.#websocket.send(json); } else { this.#queued[id] = json; @@ -297,8 +304,7 @@ export default class WsProvider implements WSProviderInterface { if (this.#autoConnectMs > 0) { setTimeout((): void => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.connect(); + this.connect().catch(() => { /* noop */ }); }, this.#autoConnectMs); } } diff --git a/packages/rpc-provider/src/ws/connect.spec.ts b/packages/rpc-provider/src/ws/connect.spec.ts index 6573316331..23d8ed3f7b 100644 --- a/packages/rpc-provider/src/ws/connect.spec.ts +++ b/packages/rpc-provider/src/ws/connect.spec.ts @@ -30,26 +30,26 @@ describe('onConnect', (): void => { it('Does not connect when autoConnect is false', (): void => { const provider: WsProvider = new WsProvider(TEST_WS_URL, 0); - expect(provider.isConnected()).toBe(false); + expect(provider.isConnected).toBe(false); }); it('Does connect when autoConnect is true', async () => { const provider: WsProvider = new WsProvider(TEST_WS_URL, 1); await sleepMs(100); // Hack to give the provider time to connect - expect(provider.isConnected()).toBe(true); + expect(provider.isConnected).toBe(true); }); it('Creates a new WebSocket instance by calling the connect() method', async () => { const provider: WsProvider = new WsProvider(TEST_WS_URL, false); - expect(provider.isConnected()).toBe(false); + expect(provider.isConnected).toBe(false); expect(mocks[0].server.clients().length).toBe(0); await provider.connect(); await sleepMs(100); // Hack to give the provider time to connect - expect(provider.isConnected()).toBe(true); + expect(provider.isConnected).toBe(true); expect(mocks[0].server.clients().length).toBe(1); }); @@ -59,7 +59,7 @@ describe('onConnect', (): void => { await provider.connect(); await sleepMs(100); // Hack to give the provider time to connect - expect(provider.isConnected()).toBe(true); + expect(provider.isConnected).toBe(true); }); it('Connects to the second endpoint when the first is unreachable', async () => { @@ -71,7 +71,7 @@ describe('onConnect', (): void => { await sleepMs(100); // Hack to give the provider time to connect - expect(provider.isConnected()).toBe(true); + expect(provider.isConnected).toBe(true); }); it('Connects to the second endpoint when the first is dropped', async () => { @@ -92,7 +92,7 @@ describe('onConnect', (): void => { // Check that second server is connected expect(mocks[1].server.clients().length).toBe(1); - expect(provider.isConnected()).toBe(true); + expect(provider.isConnected).toBe(true); }); it('Round-robin of endpoints on WsProvider', async () => { @@ -124,7 +124,7 @@ describe('onConnect', (): void => { // Check that first server is connected and the next one isn't expect(mocks[index].server.clients().length).toBe(1); expect(mockNext[index].server.clients().length).toBe(0); - expect(provider.isConnected()).toBe(true); + expect(provider.isConnected).toBe(true); // Close connection from first server mocks[index].server.clients()[0].close(); diff --git a/packages/rpc-provider/src/ws/index.spec.ts b/packages/rpc-provider/src/ws/index.spec.ts index 621617982e..e0bc099d74 100644 --- a/packages/rpc-provider/src/ws/index.spec.ts +++ b/packages/rpc-provider/src/ws/index.spec.ts @@ -25,7 +25,7 @@ describe('Ws', (): void => { it('returns the connected state', (): void => { expect( - createWs([]).isConnected() + createWs([]).isConnected ).toEqual(false); }); diff --git a/yarn.lock b/yarn.lock index d640e0cc8a..bc7a685828 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2979,12 +2979,12 @@ __metadata: languageName: unknown linkType: soft -"@polkadot/ts@npm:^0.3.40": - version: 0.3.40 - resolution: "@polkadot/ts@npm:0.3.40" +"@polkadot/ts@npm:^0.3.41": + version: 0.3.41 + resolution: "@polkadot/ts@npm:0.3.41" dependencies: "@types/chrome": ^0.0.122 - checksum: 6b1eb5217341c7f3e52221624db57eb385ab5216bd9b61129a652d784fab59895bcfadb878d35572ec816a9f3d6754e96658c0b8346cf6dcce71c42856eafb94 + checksum: 4dbcc5b8f92afa56e0030ef3fa1dfd6ffbb6154730fb53b7ad9842edf8a439e7fd3ef55d81206138cc6bc45deae67baa8b748d2f865c54e03675a85f5a9ab74e languageName: node linkType: hard @@ -16380,7 +16380,7 @@ __metadata: "@babel/register": ^7.11.5 "@babel/runtime": ^7.11.2 "@polkadot/dev": ^0.55.45 - "@polkadot/ts": ^0.3.40 + "@polkadot/ts": ^0.3.41 "@polkadot/typegen": "workspace:packages/typegen" "@types/jest": ^26.0.13 "@vuepress/plugin-search": ^1.5.4