mirror of
https://github.com/pezkuwichain/pezkuwi-api.git
synced 2026-06-13 10:51:01 +00:00
Rebrand: polkadot → pezkuwi, substrate → bizinikiwi, kusama → dicle
This commit is contained in:
@@ -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.
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
// Copyright 2017-2025 @pezkuwi/rpc-core authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export * from './index.js';
|
||||
@@ -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]);
|
||||
@@ -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' };
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user