Rebrand: polkadot → pezkuwi, substrate → bizinikiwi, kusama → dicle

This commit is contained in:
2026-01-07 02:29:40 +03:00
commit d5f038faea
1383 changed files with 1088018 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
# @pezkuwi/rpc-core
This library provides a clean wrapper around all the methods exposed by a Pezkuwi network client. It is only used internally to the API.
+35
View File
@@ -0,0 +1,35 @@
{
"author": "Jaco Greeff <jacogr@gmail.com>",
"bugs": "https://github.com/pezkuwichain/pezkuwi-api/issues",
"description": "A JavaScript wrapper for the Pezkuwi JsonRPC interface",
"engines": {
"node": ">=18"
},
"homepage": "https://github.com/pezkuwichain/pezkuwi-api/tree/master/packages/rpc-core#readme",
"license": "Apache-2.0",
"name": "@pezkuwi/rpc-core",
"repository": {
"directory": "packages/rpc-core",
"type": "git",
"url": "https://github.com/pezkuwichain/pezkuwi-api.git"
},
"sideEffects": [
"./packageDetect.js",
"./packageDetect.cjs"
],
"type": "module",
"version": "16.5.6",
"main": "index.js",
"dependencies": {
"@pezkuwi/rpc-augment": "16.5.4",
"@pezkuwi/rpc-provider": "16.5.4",
"@pezkuwi/types": "16.5.4",
"@pezkuwi/util": "^14.0.1",
"rxjs": "^7.8.1",
"tslib": "^2.8.1"
},
"devDependencies": {
"@pezkuwi/keyring": "^14.0.1",
"@pezkuwi/rpc-augment": "16.5.4"
}
}
+535
View File
@@ -0,0 +1,535 @@
// Copyright 2017-2025 @pezkuwi/rpc-core authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Observer } from 'rxjs';
import type { ProviderInterface, ProviderInterfaceCallback } from '@pezkuwi/rpc-provider/types';
import type { StorageKey, Vec } from '@pezkuwi/types';
import type { Hash } from '@pezkuwi/types/interfaces';
import type { AnyJson, AnyNumber, Codec, DefinitionRpc, DefinitionRpcExt, DefinitionRpcSub, Registry } from '@pezkuwi/types/types';
import type { Memoized } from '@pezkuwi/util/types';
import type { RpcCoreStats, RpcInterfaceMethod } from './types/index.js';
import { Observable, publishReplay, refCount } from 'rxjs';
import { LRUCache } from '@pezkuwi/rpc-provider';
import { rpcDefinitions } from '@pezkuwi/types';
import { unwrapStorageSi } from '@pezkuwi/types/util';
import { hexToU8a, isFunction, isNull, isUndefined, lazyMethod, logger, memoize, objectSpread, u8aConcat, u8aToU8a } from '@pezkuwi/util';
import { drr, refCountDelay } from './util/index.js';
export { packageInfo } from './packageInfo.js';
export * from './util/index.js';
interface StorageChangeSetJSON {
block: string;
changes: [string, string | null][];
}
type MemoizedRpcInterfaceMethod = Memoized<RpcInterfaceMethod> & {
raw: Memoized<RpcInterfaceMethod>;
meta: DefinitionRpc;
}
interface Options {
isPedantic?: boolean;
provider: ProviderInterface;
/**
* Custom size of the rpc LRUCache capacity. Defaults to `RPC_CORE_DEFAULT_CAPACITY` (1024 * 10 * 10)
*/
rpcCacheCapacity?: number;
ttl?: number | null;
userRpc?: Record<string, Record<string, DefinitionRpc | DefinitionRpcSub>>;
}
const l = logger('rpc-core');
const EMPTY_META = {
fallback: undefined,
modifier: { isOptional: true },
type: {
asMap: { linked: { isTrue: false } },
isMap: false
}
};
const RPC_CORE_DEFAULT_CAPACITY = 1024 * 10 * 10;
// utility method to create a nicely-formatted error
/** @internal */
function logErrorMessage (method: string, { noErrorLog, params, type }: DefinitionRpc, error: Error): void {
if (noErrorLog) {
return;
}
l.error(`${method}(${
params.map(({ isOptional, name, type }): string =>
`${name}${isOptional ? '?' : ''}: ${type}`
).join(', ')
}): ${type}:: ${error.message}`);
}
function isTreatAsHex (key: StorageKey): boolean {
// :code is problematic - it does not have the length attached, which is
// unlike all other storage entries where it is indeed properly encoded
return ['0x3a636f6465'].includes(key.toHex());
}
/**
* @name Rpc
* @summary The API may use a HTTP or WebSockets provider.
* @description It allows for querying a Pezkuwi Client Node.
* WebSockets provider is recommended since HTTP provider only supports basic querying.
*
* ```mermaid
* graph LR;
* A[Api] --> |WebSockets| B[WsProvider];
* B --> |endpoint| C[ws://127.0.0.1:9944]
* ```
*
* @example
* <BR>
*
* ```javascript
* import Rpc from '@pezkuwi/rpc-core';
* import { WsProvider } from '@pezkuwi/rpc-provider/ws';
*
* const provider = new WsProvider('ws://127.0.0.1:9944');
* const rpc = new Rpc(provider);
* ```
*/
export class RpcCore {
readonly #instanceId: string;
readonly #isPedantic: boolean;
readonly #registryDefault: Registry;
readonly #storageCache: LRUCache;
#storageCacheHits = 0;
#getBlockRegistry?: (blockHash: Uint8Array) => Promise<{ registry: Registry }>;
#getBlockHash?: (blockNumber: AnyNumber) => Promise<Uint8Array>;
readonly mapping = new Map<string, DefinitionRpcExt>();
readonly provider: ProviderInterface;
readonly sections: string[] = [];
/**
* @constructor
* Default constructor for the core RPC handler
* @param {Registry} registry Type Registry
* @param {ProviderInterface} options.provider An API provider using any of the supported providers (HTTP, SC or WebSocket)
* @param {number} [options.rpcCacheCapacity] Custom size of the rpc LRUCache capacity. Defaults to `RPC_CORE_DEFAULT_CAPACITY` (1024 * 10 * 10)
*/
constructor (instanceId: string, registry: Registry, { isPedantic = true, provider, rpcCacheCapacity, ttl, userRpc = {} }: Options) {
if (!provider || !isFunction(provider.send)) {
throw new Error('Expected Provider to API create');
}
this.#instanceId = instanceId;
this.#isPedantic = isPedantic;
this.#registryDefault = registry;
this.provider = provider;
const sectionNames = Object.keys(rpcDefinitions);
// these are the base keys (i.e. part of jsonrpc)
this.sections.push(...sectionNames);
this.#storageCache = new LRUCache(rpcCacheCapacity || RPC_CORE_DEFAULT_CAPACITY, ttl);
// decorate all interfaces, defined and user on this instance
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<void> {
return this.provider.connect();
}
/**
* @description Manually disconnect from the attached provider
*/
public async disconnect (): Promise<void> {
return this.provider.disconnect();
}
/**
* @description Returns the underlying core stats, including those from teh provider
*/
public get stats (): RpcCoreStats | undefined {
const stats = this.provider.stats;
return stats
? {
...stats,
core: {
cacheHits: this.#storageCacheHits,
cacheSize: this.#storageCache.length
}
}
: undefined;
}
/**
* @description Sets a registry swap (typically from Api)
*/
public setRegistrySwap (registrySwap: (blockHash: Uint8Array) => Promise<{ registry: Registry }>): void {
this.#getBlockRegistry = memoize(registrySwap, {
getInstanceId: () => this.#instanceId
});
}
/**
* @description Sets a function to resolve block hash from block number
*/
public setResolveBlockHash (resolveBlockHash: (blockNumber: AnyNumber) => Promise<Uint8Array>): void {
this.#getBlockHash = memoize(resolveBlockHash, {
getInstanceId: () => this.#instanceId
});
}
public addUserInterfaces (userRpc: Record<string, Record<string, DefinitionRpc | DefinitionRpcSub>>): void {
// add any extra user-defined sections
this.sections.push(...Object.keys(userRpc).filter((k) => !this.sections.includes(k)));
for (let s = 0, scount = this.sections.length; s < scount; s++) {
const section = this.sections[s];
const defs = objectSpread<Record<string, DefinitionRpc | DefinitionRpcSub>>({}, rpcDefinitions[section as 'babe'], userRpc[section]);
const methods = Object.keys(defs);
for (let m = 0, mcount = methods.length; m < mcount; m++) {
const method = methods[m];
const def = defs[method];
const jsonrpc = def.endpoint || `${section}_${method}`;
if (!this.mapping.has(jsonrpc)) {
const isSubscription = !!(def as DefinitionRpcSub).pubsub;
if (!(this as Record<string, unknown>)[section]) {
(this as Record<string, unknown>)[section] = {};
}
this.mapping.set(jsonrpc, objectSpread({}, def, { isSubscription, jsonrpc, method, section }));
lazyMethod(this[section as 'connect'], method, () =>
isSubscription
? this._createMethodSubscribe(section, method, def as DefinitionRpcSub)
: this._createMethodSend(section, method, def)
);
}
}
}
}
private _memomize (creator: <T> (isScale: boolean) => (...values: unknown[]) => Observable<T>, def: DefinitionRpc): MemoizedRpcInterfaceMethod {
const memoOpts = { getInstanceId: () => this.#instanceId };
const memoized = memoize(creator(true) as RpcInterfaceMethod, memoOpts);
memoized.raw = memoize(creator(false), memoOpts);
memoized.meta = def;
return memoized as MemoizedRpcInterfaceMethod;
}
private _formatResult <T> (isScale: boolean, registry: Registry, blockHash: string | Uint8Array | null | undefined, method: string, def: DefinitionRpc, params: Codec[], result: unknown): T {
return isScale
? this._formatOutput(registry, blockHash, method, def, params, result) as unknown as T
: result as T;
}
private _createMethodSend (section: string, method: string, def: DefinitionRpc): RpcInterfaceMethod {
const rpcName = def.endpoint || `${section}_${method}`;
const hashIndex = def.params.findIndex(({ isHistoric }) => isHistoric);
let memoized: null | MemoizedRpcInterfaceMethod = null;
// execute the RPC call, doing a registry swap for historic as applicable
const callWithRegistry = async <T> (isScale: boolean, values: unknown[]): Promise<T> => {
const blockId = hashIndex === -1
? null
: values[hashIndex];
const blockHash = blockId && def.params[hashIndex].type === 'BlockNumber'
? await this.#getBlockHash?.(blockId as AnyNumber)
: blockId as (Uint8Array | string | null | undefined);
const { registry } = isScale && blockHash && this.#getBlockRegistry
? await this.#getBlockRegistry(u8aToU8a(blockHash))
: { registry: this.#registryDefault };
const params = this._formatParams(registry, null, def, values);
// only cache .at(<blockHash>) queries, e.g. where valid blockHash was supplied
const result = await this.provider.send<AnyJson>(rpcName, params.map((p) => p.toJSON()), !!blockHash);
return this._formatResult(isScale, registry, blockHash, method, def, params, result);
};
const creator = <T> (isScale: boolean) => (...values: unknown[]): Observable<T> => {
const isDelayed = isScale && hashIndex !== -1 && !!values[hashIndex];
return new Observable((observer: Observer<T>): () => void => {
callWithRegistry<T>(isScale, values)
.then((value): void => {
observer.next(value);
observer.complete();
})
.catch((error: Error): void => {
logErrorMessage(method, def, error);
observer.error(error);
observer.complete();
});
return (): void => {
// delete old results from cache
if (isScale) {
memoized?.unmemoize(...values);
} else {
memoized?.raw.unmemoize(...values);
}
};
}).pipe(
// eslint-disable-next-line deprecation/deprecation
publishReplay(1), // create a Replay(1)
isDelayed
? refCountDelay() // Unsubscribe after delay
// eslint-disable-next-line deprecation/deprecation
: refCount()
);
};
memoized = this._memomize(creator, def);
return memoized;
}
// create a subscriptor, it subscribes once and resolves with the id as subscribe
private _createSubscriber ({ paramsJson, subName, subType, update }: { subType: string; subName: string; paramsJson: AnyJson[]; update: ProviderInterfaceCallback }, errorHandler: (error: Error) => void): Promise<number | string> {
return new Promise((resolve, reject): void => {
this.provider
.subscribe(subType, subName, paramsJson, update)
.then(resolve)
.catch((error: Error): void => {
errorHandler(error);
reject(error);
});
});
}
private _createMethodSubscribe (section: string, method: string, def: DefinitionRpcSub): RpcInterfaceMethod {
const [updateType, subMethod, unsubMethod] = def.pubsub;
const subName = `${section}_${subMethod}`;
const unsubName = `${section}_${unsubMethod}`;
const subType = `${section}_${updateType}`;
let memoized: null | MemoizedRpcInterfaceMethod = null;
const creator = <T> (isScale: boolean) => (...values: unknown[]): Observable<T> => {
return new Observable((observer: Observer<T>): () => void => {
// Have at least an empty promise, as used in the unsubscribe
let subscriptionPromise: Promise<number | string | null> = Promise.resolve(null);
const registry = this.#registryDefault;
const errorHandler = (error: Error): void => {
logErrorMessage(method, def, error);
observer.error(error);
};
try {
const params = this._formatParams(registry, null, def, values);
const update = (error?: Error | null, result?: unknown): void => {
if (error) {
logErrorMessage(method, def, error);
return;
}
try {
observer.next(this._formatResult(isScale, registry, null, method, def, params, result));
} catch (error) {
observer.error(error);
}
};
subscriptionPromise = this._createSubscriber({ paramsJson: params.map((p) => p.toJSON()), subName, subType, update }, errorHandler);
} catch (error) {
errorHandler(error as Error);
}
// Teardown logic
return (): void => {
// Delete from cache, so old results don't hang around
if (isScale) {
memoized?.unmemoize(...values);
} else {
memoized?.raw.unmemoize(...values);
}
// Unsubscribe from provider
subscriptionPromise
.then((subscriptionId): Promise<boolean> =>
isNull(subscriptionId)
? Promise.resolve(false)
: this.provider.unsubscribe(subType, unsubName, subscriptionId)
)
.catch((error: Error) => logErrorMessage(method, def, error));
};
}).pipe(drr());
};
memoized = this._memomize(creator, def);
return memoized;
}
private _formatParams (registry: Registry, blockHash: Uint8Array | string | null | undefined, def: DefinitionRpc, inputs: unknown[]): Codec[] {
const count = inputs.length;
const reqCount = def.params.filter(({ isOptional }) => !isOptional).length;
if (count < reqCount || count > def.params.length) {
throw new Error(`Expected ${def.params.length} parameters${reqCount === def.params.length ? '' : ` (${def.params.length - reqCount} optional)`}, ${count} found instead`);
}
const params = new Array<Codec>(count);
for (let i = 0; i < count; i++) {
params[i] = registry.createTypeUnsafe(def.params[i].type, [inputs[i]], { blockHash });
}
return params;
}
private _formatOutput (registry: Registry, blockHash: Uint8Array | string | null | undefined, method: string, rpc: DefinitionRpc, params: Codec[], result?: unknown): Codec | Codec[] {
if (rpc.type === 'StorageData') {
const key = params[0] as StorageKey;
return this._formatStorageData(registry, blockHash, key, result as string);
} else if (rpc.type === 'StorageChangeSet') {
const keys = params[0] as Vec<StorageKey>;
return keys
? this._formatStorageSet(registry, (result as StorageChangeSetJSON).block, keys, (result as StorageChangeSetJSON).changes)
: registry.createType('StorageChangeSet', result);
} else if (rpc.type === 'Vec<StorageChangeSet>') {
const jsonSet = (result as StorageChangeSetJSON[]);
const count = jsonSet.length;
const mapped = new Array<[Hash, Codec[]]>(count);
for (let i = 0; i < count; i++) {
const { block, changes } = jsonSet[i];
mapped[i] = [
registry.createType('BlockHash', block),
this._formatStorageSet(registry, block, params[0] as Vec<StorageKey>, changes)
];
}
// we only query at a specific block, not a range - flatten
return method === 'queryStorageAt'
? mapped[0][1]
: mapped as unknown as Codec[];
}
return registry.createTypeUnsafe(rpc.type, [result], { blockHash });
}
private _formatStorageData (registry: Registry, blockHash: Uint8Array | string | null | undefined, key: StorageKey, value: string | null): Codec {
const isEmpty = isNull(value);
// we convert to Uint8Array since it maps to the raw encoding, all
// data will be correctly encoded (incl. numbers, excl. :code)
const input = isEmpty
? null
: isTreatAsHex(key)
? value
: u8aToU8a(value);
return this._newType(registry, blockHash, key, input, isEmpty);
}
private _formatStorageSet (registry: Registry, blockHash: string, keys: Vec<StorageKey>, changes: [string, string | null][]): Codec[] {
// For StorageChangeSet, the changes has the [key, value] mappings
const count = keys.length;
const withCache = count !== 1;
const values = new Array<Codec>(count);
// multiple return values (via state.storage subscription), decode the
// values one at a time, all based on the supplied query types
for (let i = 0; i < count; i++) {
values[i] = this._formatStorageSetEntry(registry, blockHash, keys[i], changes, withCache, i);
}
return values;
}
private _formatStorageSetEntry (registry: Registry, blockHash: string, key: StorageKey, changes: [string, string | null][], withCache: boolean, entryIndex: number): Codec {
const hexKey = key.toHex();
const found = changes.find(([key]) => key === hexKey);
const isNotFound = isUndefined(found);
// if we don't find the value, this is our fallback
// - in the case of an array of values, fill the hole from the cache
// - if a single result value, don't fill - it is not an update hole
// - fallback to an empty option in all cases
if (isNotFound && withCache) {
const cached = this.#storageCache.get(hexKey) as Codec | undefined;
if (cached) {
this.#storageCacheHits++;
return cached;
}
}
const value = isNotFound
? null
: found[1];
const isEmpty = isNull(value);
const input = isEmpty || isTreatAsHex(key)
? value
: u8aToU8a(value);
const codec = this._newType(registry, blockHash, key, input, isEmpty, entryIndex);
this._setToCache(hexKey, codec);
return codec;
}
private _setToCache (key: string, value: Codec): void {
this.#storageCache.set(key, value);
}
private _newType (registry: Registry, blockHash: Uint8Array | string | null | undefined, key: StorageKey, input: string | Uint8Array | null, isEmpty: boolean, entryIndex = -1): Codec {
// single return value (via state.getStorage), decode the value based on the
// outputType that we have specified. Fallback to Raw on nothing
const type = key.meta ? registry.createLookupType(unwrapStorageSi(key.meta.type)) : (key.outputType || 'Raw');
const meta = key.meta || EMPTY_META;
const entryNum = entryIndex === -1
? ''
: ` entry ${entryIndex}:`;
try {
return registry.createTypeUnsafe(type, [
isEmpty
? meta.fallback
// For old-style Linkage, we add an empty linkage at the end
? type.includes('Linkage<')
? u8aConcat(hexToU8a(meta.fallback.toHex()), new Uint8Array(2))
: hexToU8a(meta.fallback.toHex())
: undefined
: meta.modifier.isOptional
? registry.createTypeUnsafe(type, [input], { blockHash, isPedantic: this.#isPedantic })
: input
], { blockHash, isFallback: isEmpty && !!meta.fallback, isOptional: meta.modifier.isOptional, isPedantic: this.#isPedantic && !meta.modifier.isOptional });
} catch (error) {
throw new Error(`Unable to decode storage ${key.section || 'unknown'}.${key.method || 'unknown'}:${entryNum}: ${(error as Error).message}`);
}
}
}
+129
View File
@@ -0,0 +1,129 @@
// Copyright 2017-2025 @pezkuwi/rpc-core authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import type { RpcInterface } from './types/index.js';
import { createTestPairs } from '@pezkuwi/keyring/testingPairs';
import { MockProvider } from '@pezkuwi/rpc-provider/mock';
import { TypeRegistry } from '@pezkuwi/types/create';
import { RpcCore } from './index.js';
describe('Cached Observables', (): void => {
const registry = new TypeRegistry();
let rpc: RpcCore & RpcInterface;
let provider: MockProvider;
const keyring = createTestPairs();
beforeEach((): void => {
provider = new MockProvider(registry);
rpc = new RpcCore('123', registry, { provider }) as (RpcCore & RpcInterface);
});
afterEach(async () => {
await provider.disconnect();
});
it('creates a single observable for subscriptions (multiple calls)', (): void => {
const observable1 = rpc.state.subscribeStorage([123]);
const observable2 = rpc.state.subscribeStorage([123]);
expect(observable2).toBe(observable1);
});
it('creates a single observable for subscriptions (multiple calls, no arguments)', (): void => {
const observable1 = rpc.chain.subscribeNewHeads();
const observable2 = rpc.chain.subscribeNewHeads();
expect(observable2).toBe(observable1);
});
it('creates a single observable (multiple calls, different arguments that should be cached together)', (): void => {
const observable1 = rpc.state.subscribeStorage([keyring.alice.address]);
const observable2 = rpc.state.subscribeStorage([registry.createType('AccountId', keyring.alice.address)]);
expect(observable2).toBe(observable1);
});
it('creates multiple observables for different values', (): void => {
const observable1 = rpc.chain.getBlockHash(123);
const observable2 = rpc.chain.getBlockHash(456);
expect(observable2).not.toBe(observable1);
});
it('subscribes to the same one if within the period (unbsub delay)', async (): Promise<void> => {
const observable1 = rpc.chain.subscribeNewHeads();
const sub1 = observable1.subscribe();
sub1.unsubscribe();
await new Promise<boolean>((resolve) => {
setTimeout((): void => {
const observable2 = rpc.chain.subscribeNewHeads();
const sub2 = observable2.subscribe();
expect(observable1).toBe(observable2);
sub2.unsubscribe();
resolve(true);
}, 500);
});
});
it('clears cache if there are no more subscribers', async (): Promise<void> => {
const observable1 = rpc.chain.subscribeNewHeads();
const observable2 = rpc.chain.subscribeNewHeads();
const sub1 = observable1.subscribe();
const sub2 = observable2.subscribe();
expect(observable1).toBe(observable2);
sub1.unsubscribe();
sub2.unsubscribe();
await new Promise<boolean>((resolve) => {
setTimeout((): void => {
// No more subscribers, now create a new observable
const observable3 = rpc.chain.subscribeNewHeads();
expect(observable3).not.toBe(observable1);
resolve(true);
}, 3500);
});
});
it('creates different observables for different methods but same arguments', (): void => {
// params do not match here
const observable1 = rpc.chain.getHeader('123');
const observable2 = rpc.chain.getBlockHash('123');
expect(observable2).not.toBe(observable1);
});
it('creates single observables for subsequent one-shots', (): void => {
const observable1 = rpc.chain.getBlockHash(123);
const observable2 = rpc.chain.getBlockHash(123);
expect(observable2).toBe(observable1);
});
it('creates multiple observables for subsequent one-shots delayed', async (): Promise<void> => {
const observable1 = rpc.chain.getBlockHash(123);
const sub = observable1.subscribe((): void => {
sub.unsubscribe();
});
expect(rpc.chain.getBlockHash(123)).toBe(observable1);
await new Promise<boolean>((resolve) => {
setTimeout((): void => {
expect(rpc.chain.getBlockHash(123)).not.toBe(observable1);
resolve(true);
}, 3500);
});
});
});
@@ -0,0 +1,4 @@
// Copyright 2017-2025 @pezkuwi/rpc-core authors & contributors
// SPDX-License-Identifier: Apache-2.0
import '@pezkuwi/rpc-augment';
+55
View File
@@ -0,0 +1,55 @@
// Copyright 2017-2025 @pezkuwi/rpc-core authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import type { ProviderInterface } from '@pezkuwi/rpc-provider/types';
import { MockProvider } from '@pezkuwi/rpc-provider/mock';
import { TypeRegistry } from '@pezkuwi/types/create';
import { isFunction } from '@pezkuwi/util';
import { RpcCore } from './index.js';
describe('Api', (): void => {
const registry = new TypeRegistry();
it('requires a provider with a send method', (): void => {
expect(
() => new RpcCore('234', registry, { provider: {} as unknown as ProviderInterface })
).toThrow(/Expected Provider/);
});
it('allows for the definition of user RPCs', async () => {
const provider = new MockProvider(registry);
const rpc = new RpcCore('567', registry, {
provider,
userRpc: {
testing: {
foo: {
description: 'foo',
params: [{ name: 'bar', type: 'u32' }],
type: 'Balance'
}
}
}
});
expect(isFunction((rpc as unknown as { testing: { foo: boolean } }).testing.foo)).toBe(true);
expect(rpc.sections.includes('testing')).toBe(true);
expect(rpc.mapping.get('testing_foo')).toEqual({
description: 'foo',
isSubscription: false,
jsonrpc: 'testing_foo',
method: 'foo',
params: [{
name: 'bar',
type: 'u32'
}],
section: 'testing',
type: 'Balance'
});
await provider.disconnect();
});
});
+6
View File
@@ -0,0 +1,6 @@
// Copyright 2017-2025 @pezkuwi/rpc-core authors & contributors
// SPDX-License-Identifier: Apache-2.0
import './packageDetect.js';
export * from './bundle.js';
+75
View File
@@ -0,0 +1,75 @@
// Copyright 2017-2025 @pezkuwi/rpc-core authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import type { ProviderInterface } from '@pezkuwi/rpc-provider/types';
import type { DefinitionRpc } from '@pezkuwi/types/types';
import { TypeRegistry } from '@pezkuwi/types/create';
import { RpcCore } from './index.js';
describe('methodSend', (): void => {
const registry = new TypeRegistry();
let rpc: RpcCore;
let methods: { blah: DefinitionRpc; bleh: DefinitionRpc };
let provider: ProviderInterface;
beforeEach((): void => {
methods = {
blah: {
description: 'test',
params: [
{ name: 'foo', type: 'Bytes' }
],
type: 'Bytes'
},
bleh: {
description: 'test',
params: [],
type: 'Bytes'
}
};
provider = {
send: jest.fn((_method: string, params: unknown[]): Promise<unknown> =>
Promise.resolve(params[0])
)
} as unknown as ProviderInterface;
rpc = new RpcCore('987', registry, { provider });
});
it('checks for mismatched parameters', async (): Promise<void> => {
// private method
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
const method = (rpc as any)._createMethodSend('test', 'bleh', methods.bleh);
await new Promise<boolean>((resolve) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
method(1).subscribe(
(): void => undefined,
(error: Error): void => {
expect(error.message).toMatch(/parameters, 1 found instead/);
resolve(true);
});
});
});
it('calls the provider with the correct parameters', async (): Promise<void> => {
// private method
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
const method = (rpc as any)._createMethodSend('test', 'blah', methods.blah);
await new Promise<boolean>((resolve) => {
// Args are length-prefixed, because it's a Bytes
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
method(new Uint8Array([2 << 2, 0x12, 0x34])).subscribe((): void => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(provider.send).toHaveBeenCalledWith('test_blah', ['0x1234'], false);
resolve(true);
});
});
});
});
+4
View File
@@ -0,0 +1,4 @@
// Copyright 2017-2025 @pezkuwi/rpc-core authors & contributors
// SPDX-License-Identifier: Apache-2.0
export * from './index.js';
+13
View File
@@ -0,0 +1,13 @@
// Copyright 2017-2026 @pezkuwi/rpc-core authors & contributors
// SPDX-License-Identifier: Apache-2.0
// Do not edit, auto-generated by @pezkuwi/dev
// (packageInfo imports will be kept as-is, user-editable)
import { packageInfo as providerInfo } from '@pezkuwi/rpc-provider/packageInfo';
import { packageInfo as typesInfo } from '@pezkuwi/types/packageInfo';
import { detectPackage } from '@pezkuwi/util';
import { packageInfo } from './packageInfo.js';
detectPackage(packageInfo, null, [providerInfo, typesInfo]);
+6
View File
@@ -0,0 +1,6 @@
// Copyright 2017-2026 @pezkuwi/rpc-core authors & contributors
// SPDX-License-Identifier: Apache-2.0
// Do not edit, auto-generated by @pezkuwi/dev
export const packageInfo = { name: '@pezkuwi/rpc-core', path: 'auto', type: 'auto', version: '16.5.4' };
+73
View File
@@ -0,0 +1,73 @@
// Copyright 2017-2025 @pezkuwi/rpc-core authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import type { RpcInterface } from './types/index.js';
import { MockProvider } from '@pezkuwi/rpc-provider/mock';
import { TypeRegistry } from '@pezkuwi/types/create';
import { RpcCore } from './index.js';
describe('replay', (): void => {
const registry = new TypeRegistry();
let rpc: RpcCore & RpcInterface;
let provider: MockProvider;
beforeEach((): void => {
provider = new MockProvider(registry);
rpc = new RpcCore('653', registry, { provider }) as (RpcCore & RpcInterface);
});
afterEach(async () => {
await provider.disconnect();
});
it('returns the observable value', async (): Promise<void> => {
await new Promise<boolean>((resolve) => {
rpc.system.chain().subscribe((value?: { toString: () => string }): void => {
if (value) {
// eslint-disable-next-line jest/no-conditional-expect
expect(value.toString()).toEqual('mockChain'); // Defined in MockProvider
resolve(true);
}
});
});
});
it('replay(1) works as expected', async (): Promise<void> => {
const observable = rpc.system.chain();
let a: any;
observable.subscribe((value?: unknown): void => {
a = value;
});
await new Promise<boolean>((resolve) => {
setTimeout((): void => {
// Subscribe again to the same observable, it should fire value immediately
observable.subscribe((value: any): void => {
expect(value).toEqual(a);
resolve(true);
});
}, 1000);
});
});
it('unsubscribes as required', async (): Promise<void> => {
rpc.provider.unsubscribe = jest.fn();
await new Promise<boolean>((resolve) => {
const subscription = rpc.chain.subscribeNewHeads().subscribe((): void => {
subscription.unsubscribe();
// There's a promise inside .unsubscribe(), wait a bit (> 2s)
setTimeout((): void => {
expect(rpc.provider.unsubscribe).toHaveBeenCalled();
resolve(true);
}, 3500);
});
});
});
});
+28
View File
@@ -0,0 +1,28 @@
// Copyright 2017-2025 @pezkuwi/rpc-core authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Observable } from 'rxjs';
import type { ProviderInterface } from '@pezkuwi/rpc-provider/types';
import type { AnyFunction, Codec, DefinitionRpc } from '@pezkuwi/types/types';
export interface RpcInterfaceMethod {
<T extends Codec> (...params: unknown[]): Observable<T>;
raw (...params: unknown[]): Observable<unknown>;
meta: DefinitionRpc;
}
export type AugmentedRpc<F extends AnyFunction> = F & {
raw: <T> (...params: Parameters<F>) => Observable<T>;
meta: DefinitionRpc;
};
/** Stats from the rpc-core layer, including the provider stats */
export interface RpcCoreStats extends NonNullable<ProviderInterface['stats']> {
/** Internal stats for the rpc-core layer */
core: {
/** The number of values retrieved from the core cache */
cacheHits: number;
/** The number of entries in the core cache */
cacheSize: number;
}
}
+8
View File
@@ -0,0 +1,8 @@
// Copyright 2017-2025 @pezkuwi/rpc-core authors & contributors
// SPDX-License-Identifier: Apache-2.0
// augmented, do an augmentation export
export * from '@pezkuwi/rpc-core/types/jsonrpc';
// normal exports
export * from './base.js';
+7
View File
@@ -0,0 +1,7 @@
// Copyright 2017-2025 @pezkuwi/rpc-core authors & contributors
// SPDX-License-Identifier: Apache-2.0
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface RpcInterface {
// augmented
}
+50
View File
@@ -0,0 +1,50 @@
// Copyright 2017-2025 @pezkuwi/api-derive authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import { of, timer } from 'rxjs';
import { drr } from './index.js';
describe('drr', (): void => {
it('should not fire twice the same value', async (): Promise<void> => {
let count = 0;
const sub = of(1, 1).pipe(drr({ delay: 500 })).subscribe((): void => {
++count;
});
await new Promise<boolean>((resolve) => {
setTimeout((): void => {
expect(count).toBe(1);
sub.unsubscribe();
setTimeout(() => {
resolve(true);
}, 2000);
}, 50);
});
});
it('should be a ReplaySubject(1)', async (): Promise<void> => {
const obs = timer(0, 100).pipe(drr({ delay: 500 })); // Starts at 0, increments every 100ms
const sub = obs.subscribe(); // Fire the observable
await new Promise<boolean>((resolve) => {
// Subscribe another time after some time, i.e. after the observable has fired
setTimeout((): void => {
const sub = obs.subscribe((value): void => {
expect(value > 1).toBe(true);
setTimeout(() => {
resolve(true);
}, 2000);
});
sub.unsubscribe();
}, 500);
sub.unsubscribe();
});
});
});
+52
View File
@@ -0,0 +1,52 @@
// Copyright 2017-2025 @pezkuwi/rpc-core authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Observable } from 'rxjs';
import { catchError, distinctUntilChanged, publishReplay, refCount, tap } from 'rxjs';
import { stringify } from '@pezkuwi/util';
import { refCountDelay } from './refCountDelay.js';
export type DrrResult = <T> (source$: Observable<T>) => Observable<T>;
interface Options {
delay?: number;
skipChange?: boolean;
skipTimeout?: boolean;
}
function CMP (a: unknown, b: unknown): boolean {
return stringify({ t: a }) === stringify({ t: b });
}
function ERR (error: Error): Observable<never> {
throw error;
}
function NOOP (): void {
// empty
}
/**
* Shorthand for distinctUntilChanged(), publishReplay(1) and refCount().
*
* @ignore
* @internal
*/
export function drr ({ delay, skipChange = false, skipTimeout = false }: Options = {}): DrrResult {
return <T> (source$: Observable<T>): Observable<T> =>
source$.pipe(
catchError(ERR),
skipChange
? tap(NOOP)
: distinctUntilChanged<T>(CMP),
// eslint-disable-next-line deprecation/deprecation
publishReplay(1),
skipTimeout
// eslint-disable-next-line deprecation/deprecation
? refCount()
: refCountDelay(delay)
);
}
+6
View File
@@ -0,0 +1,6 @@
// Copyright 2017-2025 @pezkuwi/rpc-core authors & contributors
// SPDX-License-Identifier: Apache-2.0
export * from './drr.js';
export * from './memo.js';
export * from './refCountDelay.js';
+36
View File
@@ -0,0 +1,36 @@
// Copyright 2017-2025 @pezkuwi/api-derive authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Observer, TeardownLogic } from 'rxjs';
import type { Memoized } from '@pezkuwi/util/types';
import { Observable } from 'rxjs';
import { memoize } from '@pezkuwi/util';
import { drr } from './drr.js';
type ObsFn <T> = (...params: unknown[]) => Observable<T>;
// Wraps a derive, doing 2 things to optimize calls -
// 1. creates a memo of the inner fn -> Observable, removing when unsubscribed
// 2. wraps the observable in a drr() (which includes an unsub delay)
/** @internal */
// eslint-disable-next-line @typescript-eslint/ban-types
export function memo <T> (instanceId: string, inner: Function): Memoized<ObsFn<T>> {
const options = { getInstanceId: () => instanceId };
const cached = memoize(
(...params: unknown[]): Observable<T> =>
new Observable((observer: Observer<T>): TeardownLogic => {
const subscription = (inner as ObsFn<T>)(...params).subscribe(observer);
return (): void => {
cached.unmemoize(...params);
subscription.unsubscribe();
};
}).pipe(drr()),
options
);
return cached;
}
@@ -0,0 +1,45 @@
// Copyright 2017-2025 @pezkuwi/rpc-core authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ConnectableObservable, MonoTypeOperatorFunction, TeardownLogic } from 'rxjs';
import { asapScheduler, Observable, Subscription } from 'rxjs';
/** @internal */
export function refCountDelay <T> (delay = 1750): MonoTypeOperatorFunction<T> {
return (source: Observable<T>): Observable<T> => {
// state: 0 = disconnected, 1 = disconnecting, 2 = connecting, 3 = connected
let [state, refCount, connection, scheduler] = [0, 0, Subscription.EMPTY, Subscription.EMPTY];
return new Observable((ob): TeardownLogic => {
source.subscribe(ob);
if (refCount++ === 0) {
if (state === 1) {
scheduler.unsubscribe();
} else {
// eslint-disable-next-line deprecation/deprecation
connection = (source as ConnectableObservable<T>).connect();
}
state = 3;
}
return (): void => {
if (--refCount === 0) {
if (state === 2) {
state = 0;
scheduler.unsubscribe();
} else {
// state === 3
state = 1;
scheduler = asapScheduler.schedule((): void => {
state = 0;
connection.unsubscribe();
}, delay);
}
}
};
});
};
}
+17
View File
@@ -0,0 +1,17 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": "..",
"outDir": "./build",
"rootDir": "./src"
},
"exclude": [
"**/checkTypes.manual.ts",
"**/mod.ts",
"**/*.spec.ts"
],
"references": [
{ "path": "../rpc-provider/tsconfig.build.json" },
{ "path": "../types/tsconfig.build.json" }
]
}
+20
View File
@@ -0,0 +1,20 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": "..",
"outDir": "./build",
"rootDir": "./src",
"emitDeclarationOnly": false,
"noEmit": true
},
"include": [
"**/checkTypes.manual.ts",
"**/*.spec.ts"
],
"references": [
{ "path": "../rpc-core/tsconfig.build.json" },
{ "path": "../rpc-augment/tsconfig.build.json" },
{ "path": "../rpc-provider/tsconfig.build.json" },
{ "path": "../types/tsconfig.build.json" }
]
}