Skip to content

Commit

Permalink
Enable Custom Event Subscription (#6262)
Browse files Browse the repository at this point in the history
* enable custom subscription
* refactor to simplify the inheritance from Web3Subscription
* add `CombinedEventMap` type to `Web3Subscription`
* remove unneeded casting inside `_buildSubscriptionParams`
* add a test
* fix "Version 4.x does not fire connected event for subscriptions. #6252"
* update CHANGELOG.md files
  • Loading branch information
Muhammad-Altabba authored Jul 14, 2023
1 parent 51bc03d commit e143157
Show file tree
Hide file tree
Showing 12 changed files with 212 additions and 92 deletions.
11 changes: 11 additions & 0 deletions packages/web3-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,14 @@ Documentation:
- Dependencies updated

## [Unreleased]

### Changed

- No need to pass `CommonSubscriptionEvents &` at every child class of `Web3Subscription` (#6262)
- Implementation of `_processSubscriptionResult` and `_processSubscriptionError` has been written in the base class `Web3Subscription` and maid `public`. (#6262)
- A new optional protected method `formatSubscriptionResult` could be used to customize data formatting instead of re-implementing `_processSubscriptionResult`. (#6262)
- No more needed to pass `CommonSubscriptionEvents & ` for the first generic parameter of `Web3Subscription` when inheriting from it. (#6262)

### Fixed

- Fixed the issue: "Version 4.x does not fire connected event for subscriptions. #6252". (#6262)
15 changes: 8 additions & 7 deletions packages/web3-core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,21 @@ export const isLegacySendAsyncProvider = <API extends Web3APISpec>(
export const isSupportedProvider = <API extends Web3APISpec>(
provider: SupportedProviders<API>,
): provider is SupportedProviders<API> =>
isWeb3Provider(provider) ||
isEIP1193Provider(provider) ||
isLegacyRequestProvider(provider) ||
isLegacySendAsyncProvider(provider) ||
isLegacySendProvider(provider);
provider &&
(isWeb3Provider(provider) ||
isEIP1193Provider(provider) ||
isLegacyRequestProvider(provider) ||
isLegacySendAsyncProvider(provider) ||
isLegacySendProvider(provider));

export const isSupportSubscriptions = <API extends Web3APISpec>(
provider: SupportedProviders<API>,
): boolean => {
if (isWeb3Provider<API>(provider)) {
if (provider && 'supportsSubscriptions' in provider) {
return provider.supportsSubscriptions();
}

if (typeof provider !== 'string' && 'on' in provider) {
if (provider && typeof provider !== 'string' && 'on' in provider) {
return true;
}

Expand Down
30 changes: 23 additions & 7 deletions packages/web3-core/src/web3_subscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,23 @@ import { Web3SubscriptionManager } from './web3_subscription_manager.js';
import { Web3EventEmitter, Web3EventMap } from './web3_event_emitter.js';
import { Web3RequestManager } from './web3_request_manager.js';

type CommonSubscriptionEvents = {
data: unknown; // Fires on each incoming block header.
error: Error; // Fires when an error in the subscription occurs.
connected: string; // Fires once after the subscription successfully connected. Returns the subscription id.
};

export abstract class Web3Subscription<
EventMap extends Web3EventMap,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ArgsType = any,
API extends Web3APISpec = EthExecutionAPI,
> extends Web3EventEmitter<EventMap> {
// The following generic type is just to define the type `CombinedEventMap` and use it inside the class
// it combines the user passed `EventMap` with the `CommonSubscriptionEvents`
// However, this type definition could be refactored depending on the closure of
// [Permit type alias declarations inside a class](https://github.com/microsoft/TypeScript/issues/7061)
CombinedEventMap extends CommonSubscriptionEvents = EventMap & CommonSubscriptionEvents,
> extends Web3EventEmitter<CombinedEventMap> {
private readonly _subscriptionManager: Web3SubscriptionManager<API>;
private readonly _lastBlock?: BlockOutput;
private readonly _returnFormat: DataFormat;
Expand Down Expand Up @@ -127,6 +138,8 @@ export abstract class Web3Subscription<
method: 'eth_subscribe',
params: this._buildSubscriptionParams(),
});

this.emit('connected', this._id);
return this._id;
}

Expand Down Expand Up @@ -154,14 +167,17 @@ export abstract class Web3Subscription<
this._id = undefined;
}

// eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars
protected _processSubscriptionResult(_data: unknown) {
// Do nothing - This should be overridden in subclass.
// eslint-disable-next-line class-methods-use-this
protected formatSubscriptionResult(data: CombinedEventMap['data']) {
return data;
}

public _processSubscriptionResult(data: CombinedEventMap['data'] | unknown) {
this.emit('data', this.formatSubscriptionResult(data));
}

// eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars
protected _processSubscriptionError(_err: Error) {
// Do nothing - This should be overridden in subclass.
public _processSubscriptionError(error: Error) {
this.emit('error', error);
}

// eslint-disable-next-line class-methods-use-this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ export class ExampleSubscription extends Web3Subscription<
> {
// eslint-disable-next-line class-methods-use-this
protected _buildSubscriptionParams() {
return ['newHeads'] as ['newHeads'];
return ['newHeads'];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ describe('Web3Subscription', () => {
},
},
};
// @ts-expect-error spy on protected method
const processResult = jest.spyOn(sub, '_processSubscriptionResult');
provider.emit('data', testData);
expect(processResult).toHaveBeenCalledWith(testData.data.result);
Expand All @@ -80,7 +79,6 @@ describe('Web3Subscription', () => {
},
},
};
// @ts-expect-error spy on protected method
const processResult = jest.spyOn(sub, '_processSubscriptionResult');
provider.emit('data', testData);
expect(processResult).toHaveBeenCalledWith(testData.data);
Expand All @@ -99,7 +97,6 @@ describe('Web3Subscription', () => {
},
},
};
// @ts-expect-error spy on protected method
const processResult = jest.spyOn(sub, '_processSubscriptionResult');
eipProvider.emit('message', testData);
expect(processResult).toHaveBeenCalledWith(testData.data.result);
Expand All @@ -119,7 +116,6 @@ describe('Web3Subscription', () => {
},
},
};
// @ts-expect-error spy on protected method
const processResult = jest.spyOn(sub, '_processSubscriptionResult');
eipProvider.emit('message', testData);
expect(processResult).toHaveBeenCalledWith(testData.data);
Expand Down
12 changes: 3 additions & 9 deletions packages/web3-eth-contract/src/log_subscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,6 @@ import { EventLog, ContractAbiWithSignature } from './types.js';
*/
export class LogsSubscription extends Web3Subscription<
{
error: Error;
connected: number;
data: EventLog;
changed: EventLog & { removed: true };
},
Expand Down Expand Up @@ -152,14 +150,10 @@ export class LogsSubscription extends Web3Subscription<
}

protected _buildSubscriptionParams() {
return ['logs', { address: this.address, topics: this.topics }] as [
'logs',
{ address?: string; topics?: string[] },
];
return ['logs', { address: this.address, topics: this.topics }];
}

protected _processSubscriptionResult(data: LogsInput): void {
const decoded = decodeEventABI(this.abi, data, this.jsonInterface, super.returnFormat);
this.emit('data', decoded);
protected formatSubscriptionResult(data: EventLog) {
return decodeEventABI(this.abi, data as LogsInput, this.jsonInterface, super.returnFormat);
}
}
2 changes: 1 addition & 1 deletion packages/web3-eth/src/web3_eth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ import {
SyncingSubscription,
} from './web3_subscriptions.js';

type RegisteredSubscription = {
export type RegisteredSubscription = {
logs: typeof LogsSubscription;
newPendingTransactions: typeof NewPendingTransactionsSubscription;
pendingTransactions: typeof NewPendingTransactionsSubscription;
Expand Down
71 changes: 22 additions & 49 deletions packages/web3-eth/src/web3_subscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,6 @@ import {
import { Web3Subscription } from 'web3-core';
import { blockHeaderSchema, logSchema, syncSchema } from './schemas.js';

type CommonSubscriptionEvents = {
error: Error;
connected: number;
};
/**
* ## subscribe('logs')
* Subscribes to incoming logs, filtered by the given options. If a valid numerical fromBlock options property is set, web3.js will retrieve logs beginning from this point, backfilling the response as necessary.
Expand All @@ -44,7 +40,7 @@ type CommonSubscriptionEvents = {
*
*/
export class LogsSubscription extends Web3Subscription<
CommonSubscriptionEvents & {
{
data: LogsOutput;
},
{
Expand All @@ -54,16 +50,11 @@ export class LogsSubscription extends Web3Subscription<
}
> {
protected _buildSubscriptionParams() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return ['logs', this.args] as ['logs', any];
}

public _processSubscriptionResult(data: LogsOutput) {
this.emit('data', format(logSchema, data, super.returnFormat));
return ['logs', this.args];
}

public _processSubscriptionError(error: Error) {
this.emit('error', error);
protected formatSubscriptionResult(data: LogsOutput) {
return format(logSchema, data, super.returnFormat);
}
}

Expand All @@ -77,22 +68,16 @@ export class LogsSubscription extends Web3Subscription<
* (await web3.eth.subscribe('pendingTransactions')).on('data', console.log);
* ```
*/
export class NewPendingTransactionsSubscription extends Web3Subscription<
CommonSubscriptionEvents & {
data: HexString;
}
> {
export class NewPendingTransactionsSubscription extends Web3Subscription<{
data: HexString;
}> {
// eslint-disable-next-line
protected _buildSubscriptionParams() {
return ['newPendingTransactions'] as ['newPendingTransactions'];
}

protected _processSubscriptionResult(data: string) {
this.emit('data', format({ format: 'string' }, data, super.returnFormat));
return ['newPendingTransactions'];
}

protected _processSubscriptionError(error: Error) {
this.emit('error', error);
protected formatSubscriptionResult(data: string) {
return format({ format: 'string' }, data, super.returnFormat);
}
}

Expand Down Expand Up @@ -125,22 +110,16 @@ export class NewPendingTransactionsSubscription extends Web3Subscription<
* }
* ```
*/
export class NewHeadsSubscription extends Web3Subscription<
CommonSubscriptionEvents & {
data: BlockHeaderOutput;
}
> {
export class NewHeadsSubscription extends Web3Subscription<{
data: BlockHeaderOutput;
}> {
// eslint-disable-next-line
protected _buildSubscriptionParams() {
return ['newHeads'] as ['newHeads'];
}

protected _processSubscriptionResult(data: BlockHeaderOutput) {
this.emit('data', format(blockHeaderSchema, data, super.returnFormat));
return ['newHeads'];
}

protected _processSubscriptionError(error: Error) {
this.emit('error', error);
protected formatSubscriptionResult(data: BlockHeaderOutput) {
return format(blockHeaderSchema, data, super.returnFormat);
}
}

Expand All @@ -163,18 +142,16 @@ export class NewHeadsSubscription extends Web3Subscription<
* }
* ```
*/
export class SyncingSubscription extends Web3Subscription<
CommonSubscriptionEvents & {
data: SyncOutput;
changed: boolean;
}
> {
export class SyncingSubscription extends Web3Subscription<{
data: SyncOutput;
changed: boolean;
}> {
// eslint-disable-next-line
protected _buildSubscriptionParams() {
return ['syncing'] as ['syncing'];
return ['syncing'];
}

protected _processSubscriptionResult(
public _processSubscriptionResult(
data:
| {
syncing: boolean;
Expand All @@ -196,8 +173,4 @@ export class SyncingSubscription extends Web3Subscription<
this.emit('data', format(syncSchema, mappedData, super.returnFormat));
}
}

protected _processSubscriptionError(error: Error) {
this.emit('error', error);
}
}
2 changes: 1 addition & 1 deletion packages/web3-eth/test/fixtures/example_subscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ export class NewHeadsSubscription extends Web3Subscription<
> {
// eslint-disable-next-line class-methods-use-this
protected _buildSubscriptionParams() {
return ['newHeads'] as ['newHeads'];
return ['newHeads'];
}
}
4 changes: 4 additions & 0 deletions packages/web3/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ Documentation:

## [4.0.3]

## Added

- Web3 constructor accepts `Web3ContextInitOptions<EthExecutionAPI, CustomRegisteredSubscription>` as alternative to the still supported `undefined`, `string`, and `SupportedProviders<EthExecutionAPI>` (#6262).

### Fixed

- Fixed bug #6236 by adding personal type in web3.eth (#6245)
Expand Down
Loading

0 comments on commit e143157

Please sign in to comment.