@pezkuwi/rpc-core 16.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/bundle.ts ADDED
@@ -0,0 +1,535 @@
1
+ // Copyright 2017-2025 @polkadot/rpc-core authors & contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import type { Observer } from 'rxjs';
5
+ import type { ProviderInterface, ProviderInterfaceCallback } from '@pezkuwi/rpc-provider/types';
6
+ import type { StorageKey, Vec } from '@pezkuwi/types';
7
+ import type { Hash } from '@pezkuwi/types/interfaces';
8
+ import type { AnyJson, AnyNumber, Codec, DefinitionRpc, DefinitionRpcExt, DefinitionRpcSub, Registry } from '@pezkuwi/types/types';
9
+ import type { Memoized } from '@pezkuwi/util/types';
10
+ import type { RpcCoreStats, RpcInterfaceMethod } from './types/index.js';
11
+
12
+ import { Observable, publishReplay, refCount } from 'rxjs';
13
+
14
+ import { LRUCache } from '@pezkuwi/rpc-provider';
15
+ import { rpcDefinitions } from '@pezkuwi/types';
16
+ import { unwrapStorageSi } from '@pezkuwi/types/util';
17
+ import { hexToU8a, isFunction, isNull, isUndefined, lazyMethod, logger, memoize, objectSpread, u8aConcat, u8aToU8a } from '@pezkuwi/util';
18
+
19
+ import { drr, refCountDelay } from './util/index.js';
20
+
21
+ export { packageInfo } from './packageInfo.js';
22
+ export * from './util/index.js';
23
+
24
+ interface StorageChangeSetJSON {
25
+ block: string;
26
+ changes: [string, string | null][];
27
+ }
28
+
29
+ type MemoizedRpcInterfaceMethod = Memoized<RpcInterfaceMethod> & {
30
+ raw: Memoized<RpcInterfaceMethod>;
31
+ meta: DefinitionRpc;
32
+ }
33
+
34
+ interface Options {
35
+ isPedantic?: boolean;
36
+ provider: ProviderInterface;
37
+ /**
38
+ * Custom size of the rpc LRUCache capacity. Defaults to `RPC_CORE_DEFAULT_CAPACITY` (1024 * 10 * 10)
39
+ */
40
+ rpcCacheCapacity?: number;
41
+ ttl?: number | null;
42
+ userRpc?: Record<string, Record<string, DefinitionRpc | DefinitionRpcSub>>;
43
+ }
44
+
45
+ const l = logger('rpc-core');
46
+
47
+ const EMPTY_META = {
48
+ fallback: undefined,
49
+ modifier: { isOptional: true },
50
+ type: {
51
+ asMap: { linked: { isTrue: false } },
52
+ isMap: false
53
+ }
54
+ };
55
+
56
+ const RPC_CORE_DEFAULT_CAPACITY = 1024 * 10 * 10;
57
+
58
+ // utility method to create a nicely-formatted error
59
+ /** @internal */
60
+ function logErrorMessage (method: string, { noErrorLog, params, type }: DefinitionRpc, error: Error): void {
61
+ if (noErrorLog) {
62
+ return;
63
+ }
64
+
65
+ l.error(`${method}(${
66
+ params.map(({ isOptional, name, type }): string =>
67
+ `${name}${isOptional ? '?' : ''}: ${type}`
68
+ ).join(', ')
69
+ }): ${type}:: ${error.message}`);
70
+ }
71
+
72
+ function isTreatAsHex (key: StorageKey): boolean {
73
+ // :code is problematic - it does not have the length attached, which is
74
+ // unlike all other storage entries where it is indeed properly encoded
75
+ return ['0x3a636f6465'].includes(key.toHex());
76
+ }
77
+
78
+ /**
79
+ * @name Rpc
80
+ * @summary The API may use a HTTP or WebSockets provider.
81
+ * @description It allows for querying a Polkadot Client Node.
82
+ * WebSockets provider is recommended since HTTP provider only supports basic querying.
83
+ *
84
+ * ```mermaid
85
+ * graph LR;
86
+ * A[Api] --> |WebSockets| B[WsProvider];
87
+ * B --> |endpoint| C[ws://127.0.0.1:9944]
88
+ * ```
89
+ *
90
+ * @example
91
+ * <BR>
92
+ *
93
+ * ```javascript
94
+ * import Rpc from '@pezkuwi/rpc-core';
95
+ * import { WsProvider } from '@pezkuwi/rpc-provider/ws';
96
+ *
97
+ * const provider = new WsProvider('ws://127.0.0.1:9944');
98
+ * const rpc = new Rpc(provider);
99
+ * ```
100
+ */
101
+ export class RpcCore {
102
+ readonly #instanceId: string;
103
+ readonly #isPedantic: boolean;
104
+ readonly #registryDefault: Registry;
105
+ readonly #storageCache: LRUCache;
106
+ #storageCacheHits = 0;
107
+
108
+ #getBlockRegistry?: (blockHash: Uint8Array) => Promise<{ registry: Registry }>;
109
+ #getBlockHash?: (blockNumber: AnyNumber) => Promise<Uint8Array>;
110
+
111
+ readonly mapping = new Map<string, DefinitionRpcExt>();
112
+ readonly provider: ProviderInterface;
113
+ readonly sections: string[] = [];
114
+
115
+ /**
116
+ * @constructor
117
+ * Default constructor for the core RPC handler
118
+ * @param {Registry} registry Type Registry
119
+ * @param {ProviderInterface} options.provider An API provider using any of the supported providers (HTTP, SC or WebSocket)
120
+ * @param {number} [options.rpcCacheCapacity] Custom size of the rpc LRUCache capacity. Defaults to `RPC_CORE_DEFAULT_CAPACITY` (1024 * 10 * 10)
121
+ */
122
+ constructor (instanceId: string, registry: Registry, { isPedantic = true, provider, rpcCacheCapacity, ttl, userRpc = {} }: Options) {
123
+ if (!provider || !isFunction(provider.send)) {
124
+ throw new Error('Expected Provider to API create');
125
+ }
126
+
127
+ this.#instanceId = instanceId;
128
+ this.#isPedantic = isPedantic;
129
+ this.#registryDefault = registry;
130
+ this.provider = provider;
131
+
132
+ const sectionNames = Object.keys(rpcDefinitions);
133
+
134
+ // these are the base keys (i.e. part of jsonrpc)
135
+ this.sections.push(...sectionNames);
136
+ this.#storageCache = new LRUCache(rpcCacheCapacity || RPC_CORE_DEFAULT_CAPACITY, ttl);
137
+ // decorate all interfaces, defined and user on this instance
138
+ this.addUserInterfaces(userRpc);
139
+ }
140
+
141
+ /**
142
+ * @description Returns the connected status of a provider
143
+ */
144
+ public get isConnected (): boolean {
145
+ return this.provider.isConnected;
146
+ }
147
+
148
+ /**
149
+ * @description Manually connect from the attached provider
150
+ */
151
+ public connect (): Promise<void> {
152
+ return this.provider.connect();
153
+ }
154
+
155
+ /**
156
+ * @description Manually disconnect from the attached provider
157
+ */
158
+ public async disconnect (): Promise<void> {
159
+ return this.provider.disconnect();
160
+ }
161
+
162
+ /**
163
+ * @description Returns the underlying core stats, including those from teh provider
164
+ */
165
+ public get stats (): RpcCoreStats | undefined {
166
+ const stats = this.provider.stats;
167
+
168
+ return stats
169
+ ? {
170
+ ...stats,
171
+ core: {
172
+ cacheHits: this.#storageCacheHits,
173
+ cacheSize: this.#storageCache.length
174
+ }
175
+ }
176
+ : undefined;
177
+ }
178
+
179
+ /**
180
+ * @description Sets a registry swap (typically from Api)
181
+ */
182
+ public setRegistrySwap (registrySwap: (blockHash: Uint8Array) => Promise<{ registry: Registry }>): void {
183
+ this.#getBlockRegistry = memoize(registrySwap, {
184
+ getInstanceId: () => this.#instanceId
185
+ });
186
+ }
187
+
188
+ /**
189
+ * @description Sets a function to resolve block hash from block number
190
+ */
191
+ public setResolveBlockHash (resolveBlockHash: (blockNumber: AnyNumber) => Promise<Uint8Array>): void {
192
+ this.#getBlockHash = memoize(resolveBlockHash, {
193
+ getInstanceId: () => this.#instanceId
194
+ });
195
+ }
196
+
197
+ public addUserInterfaces (userRpc: Record<string, Record<string, DefinitionRpc | DefinitionRpcSub>>): void {
198
+ // add any extra user-defined sections
199
+ this.sections.push(...Object.keys(userRpc).filter((k) => !this.sections.includes(k)));
200
+
201
+ for (let s = 0, scount = this.sections.length; s < scount; s++) {
202
+ const section = this.sections[s];
203
+ const defs = objectSpread<Record<string, DefinitionRpc | DefinitionRpcSub>>({}, rpcDefinitions[section as 'babe'], userRpc[section]);
204
+ const methods = Object.keys(defs);
205
+
206
+ for (let m = 0, mcount = methods.length; m < mcount; m++) {
207
+ const method = methods[m];
208
+ const def = defs[method];
209
+ const jsonrpc = def.endpoint || `${section}_${method}`;
210
+
211
+ if (!this.mapping.has(jsonrpc)) {
212
+ const isSubscription = !!(def as DefinitionRpcSub).pubsub;
213
+
214
+ if (!(this as Record<string, unknown>)[section]) {
215
+ (this as Record<string, unknown>)[section] = {};
216
+ }
217
+
218
+ this.mapping.set(jsonrpc, objectSpread({}, def, { isSubscription, jsonrpc, method, section }));
219
+
220
+ lazyMethod(this[section as 'connect'], method, () =>
221
+ isSubscription
222
+ ? this._createMethodSubscribe(section, method, def as DefinitionRpcSub)
223
+ : this._createMethodSend(section, method, def)
224
+ );
225
+ }
226
+ }
227
+ }
228
+ }
229
+
230
+ private _memomize (creator: <T> (isScale: boolean) => (...values: unknown[]) => Observable<T>, def: DefinitionRpc): MemoizedRpcInterfaceMethod {
231
+ const memoOpts = { getInstanceId: () => this.#instanceId };
232
+ const memoized = memoize(creator(true) as RpcInterfaceMethod, memoOpts);
233
+
234
+ memoized.raw = memoize(creator(false), memoOpts);
235
+ memoized.meta = def;
236
+
237
+ return memoized as MemoizedRpcInterfaceMethod;
238
+ }
239
+
240
+ private _formatResult <T> (isScale: boolean, registry: Registry, blockHash: string | Uint8Array | null | undefined, method: string, def: DefinitionRpc, params: Codec[], result: unknown): T {
241
+ return isScale
242
+ ? this._formatOutput(registry, blockHash, method, def, params, result) as unknown as T
243
+ : result as T;
244
+ }
245
+
246
+ private _createMethodSend (section: string, method: string, def: DefinitionRpc): RpcInterfaceMethod {
247
+ const rpcName = def.endpoint || `${section}_${method}`;
248
+ const hashIndex = def.params.findIndex(({ isHistoric }) => isHistoric);
249
+ let memoized: null | MemoizedRpcInterfaceMethod = null;
250
+
251
+ // execute the RPC call, doing a registry swap for historic as applicable
252
+ const callWithRegistry = async <T> (isScale: boolean, values: unknown[]): Promise<T> => {
253
+ const blockId = hashIndex === -1
254
+ ? null
255
+ : values[hashIndex];
256
+
257
+ const blockHash = blockId && def.params[hashIndex].type === 'BlockNumber'
258
+ ? await this.#getBlockHash?.(blockId as AnyNumber)
259
+ : blockId as (Uint8Array | string | null | undefined);
260
+
261
+ const { registry } = isScale && blockHash && this.#getBlockRegistry
262
+ ? await this.#getBlockRegistry(u8aToU8a(blockHash))
263
+ : { registry: this.#registryDefault };
264
+
265
+ const params = this._formatParams(registry, null, def, values);
266
+
267
+ // only cache .at(<blockHash>) queries, e.g. where valid blockHash was supplied
268
+ const result = await this.provider.send<AnyJson>(rpcName, params.map((p) => p.toJSON()), !!blockHash);
269
+
270
+ return this._formatResult(isScale, registry, blockHash, method, def, params, result);
271
+ };
272
+
273
+ const creator = <T> (isScale: boolean) => (...values: unknown[]): Observable<T> => {
274
+ const isDelayed = isScale && hashIndex !== -1 && !!values[hashIndex];
275
+
276
+ return new Observable((observer: Observer<T>): () => void => {
277
+ callWithRegistry<T>(isScale, values)
278
+ .then((value): void => {
279
+ observer.next(value);
280
+ observer.complete();
281
+ })
282
+ .catch((error: Error): void => {
283
+ logErrorMessage(method, def, error);
284
+
285
+ observer.error(error);
286
+ observer.complete();
287
+ });
288
+
289
+ return (): void => {
290
+ // delete old results from cache
291
+ if (isScale) {
292
+ memoized?.unmemoize(...values);
293
+ } else {
294
+ memoized?.raw.unmemoize(...values);
295
+ }
296
+ };
297
+ }).pipe(
298
+ // eslint-disable-next-line deprecation/deprecation
299
+ publishReplay(1), // create a Replay(1)
300
+ isDelayed
301
+ ? refCountDelay() // Unsubscribe after delay
302
+ // eslint-disable-next-line deprecation/deprecation
303
+ : refCount()
304
+ );
305
+ };
306
+
307
+ memoized = this._memomize(creator, def);
308
+
309
+ return memoized;
310
+ }
311
+
312
+ // create a subscriptor, it subscribes once and resolves with the id as subscribe
313
+ private _createSubscriber ({ paramsJson, subName, subType, update }: { subType: string; subName: string; paramsJson: AnyJson[]; update: ProviderInterfaceCallback }, errorHandler: (error: Error) => void): Promise<number | string> {
314
+ return new Promise((resolve, reject): void => {
315
+ this.provider
316
+ .subscribe(subType, subName, paramsJson, update)
317
+ .then(resolve)
318
+ .catch((error: Error): void => {
319
+ errorHandler(error);
320
+ reject(error);
321
+ });
322
+ });
323
+ }
324
+
325
+ private _createMethodSubscribe (section: string, method: string, def: DefinitionRpcSub): RpcInterfaceMethod {
326
+ const [updateType, subMethod, unsubMethod] = def.pubsub;
327
+ const subName = `${section}_${subMethod}`;
328
+ const unsubName = `${section}_${unsubMethod}`;
329
+ const subType = `${section}_${updateType}`;
330
+ let memoized: null | MemoizedRpcInterfaceMethod = null;
331
+
332
+ const creator = <T> (isScale: boolean) => (...values: unknown[]): Observable<T> => {
333
+ return new Observable((observer: Observer<T>): () => void => {
334
+ // Have at least an empty promise, as used in the unsubscribe
335
+ let subscriptionPromise: Promise<number | string | null> = Promise.resolve(null);
336
+ const registry = this.#registryDefault;
337
+
338
+ const errorHandler = (error: Error): void => {
339
+ logErrorMessage(method, def, error);
340
+
341
+ observer.error(error);
342
+ };
343
+
344
+ try {
345
+ const params = this._formatParams(registry, null, def, values);
346
+
347
+ const update = (error?: Error | null, result?: unknown): void => {
348
+ if (error) {
349
+ logErrorMessage(method, def, error);
350
+
351
+ return;
352
+ }
353
+
354
+ try {
355
+ observer.next(this._formatResult(isScale, registry, null, method, def, params, result));
356
+ } catch (error) {
357
+ observer.error(error);
358
+ }
359
+ };
360
+
361
+ subscriptionPromise = this._createSubscriber({ paramsJson: params.map((p) => p.toJSON()), subName, subType, update }, errorHandler);
362
+ } catch (error) {
363
+ errorHandler(error as Error);
364
+ }
365
+
366
+ // Teardown logic
367
+ return (): void => {
368
+ // Delete from cache, so old results don't hang around
369
+ if (isScale) {
370
+ memoized?.unmemoize(...values);
371
+ } else {
372
+ memoized?.raw.unmemoize(...values);
373
+ }
374
+
375
+ // Unsubscribe from provider
376
+ subscriptionPromise
377
+ .then((subscriptionId): Promise<boolean> =>
378
+ isNull(subscriptionId)
379
+ ? Promise.resolve(false)
380
+ : this.provider.unsubscribe(subType, unsubName, subscriptionId)
381
+ )
382
+ .catch((error: Error) => logErrorMessage(method, def, error));
383
+ };
384
+ }).pipe(drr());
385
+ };
386
+
387
+ memoized = this._memomize(creator, def);
388
+
389
+ return memoized;
390
+ }
391
+
392
+ private _formatParams (registry: Registry, blockHash: Uint8Array | string | null | undefined, def: DefinitionRpc, inputs: unknown[]): Codec[] {
393
+ const count = inputs.length;
394
+ const reqCount = def.params.filter(({ isOptional }) => !isOptional).length;
395
+
396
+ if (count < reqCount || count > def.params.length) {
397
+ throw new Error(`Expected ${def.params.length} parameters${reqCount === def.params.length ? '' : ` (${def.params.length - reqCount} optional)`}, ${count} found instead`);
398
+ }
399
+
400
+ const params = new Array<Codec>(count);
401
+
402
+ for (let i = 0; i < count; i++) {
403
+ params[i] = registry.createTypeUnsafe(def.params[i].type, [inputs[i]], { blockHash });
404
+ }
405
+
406
+ return params;
407
+ }
408
+
409
+ private _formatOutput (registry: Registry, blockHash: Uint8Array | string | null | undefined, method: string, rpc: DefinitionRpc, params: Codec[], result?: unknown): Codec | Codec[] {
410
+ if (rpc.type === 'StorageData') {
411
+ const key = params[0] as StorageKey;
412
+
413
+ return this._formatStorageData(registry, blockHash, key, result as string);
414
+ } else if (rpc.type === 'StorageChangeSet') {
415
+ const keys = params[0] as Vec<StorageKey>;
416
+
417
+ return keys
418
+ ? this._formatStorageSet(registry, (result as StorageChangeSetJSON).block, keys, (result as StorageChangeSetJSON).changes)
419
+ : registry.createType('StorageChangeSet', result);
420
+ } else if (rpc.type === 'Vec<StorageChangeSet>') {
421
+ const jsonSet = (result as StorageChangeSetJSON[]);
422
+ const count = jsonSet.length;
423
+ const mapped = new Array<[Hash, Codec[]]>(count);
424
+
425
+ for (let i = 0; i < count; i++) {
426
+ const { block, changes } = jsonSet[i];
427
+
428
+ mapped[i] = [
429
+ registry.createType('BlockHash', block),
430
+ this._formatStorageSet(registry, block, params[0] as Vec<StorageKey>, changes)
431
+ ];
432
+ }
433
+
434
+ // we only query at a specific block, not a range - flatten
435
+ return method === 'queryStorageAt'
436
+ ? mapped[0][1]
437
+ : mapped as unknown as Codec[];
438
+ }
439
+
440
+ return registry.createTypeUnsafe(rpc.type, [result], { blockHash });
441
+ }
442
+
443
+ private _formatStorageData (registry: Registry, blockHash: Uint8Array | string | null | undefined, key: StorageKey, value: string | null): Codec {
444
+ const isEmpty = isNull(value);
445
+
446
+ // we convert to Uint8Array since it maps to the raw encoding, all
447
+ // data will be correctly encoded (incl. numbers, excl. :code)
448
+ const input = isEmpty
449
+ ? null
450
+ : isTreatAsHex(key)
451
+ ? value
452
+ : u8aToU8a(value);
453
+
454
+ return this._newType(registry, blockHash, key, input, isEmpty);
455
+ }
456
+
457
+ private _formatStorageSet (registry: Registry, blockHash: string, keys: Vec<StorageKey>, changes: [string, string | null][]): Codec[] {
458
+ // For StorageChangeSet, the changes has the [key, value] mappings
459
+ const count = keys.length;
460
+ const withCache = count !== 1;
461
+ const values = new Array<Codec>(count);
462
+
463
+ // multiple return values (via state.storage subscription), decode the
464
+ // values one at a time, all based on the supplied query types
465
+ for (let i = 0; i < count; i++) {
466
+ values[i] = this._formatStorageSetEntry(registry, blockHash, keys[i], changes, withCache, i);
467
+ }
468
+
469
+ return values;
470
+ }
471
+
472
+ private _formatStorageSetEntry (registry: Registry, blockHash: string, key: StorageKey, changes: [string, string | null][], withCache: boolean, entryIndex: number): Codec {
473
+ const hexKey = key.toHex();
474
+ const found = changes.find(([key]) => key === hexKey);
475
+ const isNotFound = isUndefined(found);
476
+
477
+ // if we don't find the value, this is our fallback
478
+ // - in the case of an array of values, fill the hole from the cache
479
+ // - if a single result value, don't fill - it is not an update hole
480
+ // - fallback to an empty option in all cases
481
+ if (isNotFound && withCache) {
482
+ const cached = this.#storageCache.get(hexKey) as Codec | undefined;
483
+
484
+ if (cached) {
485
+ this.#storageCacheHits++;
486
+
487
+ return cached;
488
+ }
489
+ }
490
+
491
+ const value = isNotFound
492
+ ? null
493
+ : found[1];
494
+ const isEmpty = isNull(value);
495
+ const input = isEmpty || isTreatAsHex(key)
496
+ ? value
497
+ : u8aToU8a(value);
498
+ const codec = this._newType(registry, blockHash, key, input, isEmpty, entryIndex);
499
+
500
+ this._setToCache(hexKey, codec);
501
+
502
+ return codec;
503
+ }
504
+
505
+ private _setToCache (key: string, value: Codec): void {
506
+ this.#storageCache.set(key, value);
507
+ }
508
+
509
+ private _newType (registry: Registry, blockHash: Uint8Array | string | null | undefined, key: StorageKey, input: string | Uint8Array | null, isEmpty: boolean, entryIndex = -1): Codec {
510
+ // single return value (via state.getStorage), decode the value based on the
511
+ // outputType that we have specified. Fallback to Raw on nothing
512
+ const type = key.meta ? registry.createLookupType(unwrapStorageSi(key.meta.type)) : (key.outputType || 'Raw');
513
+ const meta = key.meta || EMPTY_META;
514
+ const entryNum = entryIndex === -1
515
+ ? ''
516
+ : ` entry ${entryIndex}:`;
517
+
518
+ try {
519
+ return registry.createTypeUnsafe(type, [
520
+ isEmpty
521
+ ? meta.fallback
522
+ // For old-style Linkage, we add an empty linkage at the end
523
+ ? type.includes('Linkage<')
524
+ ? u8aConcat(hexToU8a(meta.fallback.toHex()), new Uint8Array(2))
525
+ : hexToU8a(meta.fallback.toHex())
526
+ : undefined
527
+ : meta.modifier.isOptional
528
+ ? registry.createTypeUnsafe(type, [input], { blockHash, isPedantic: this.#isPedantic })
529
+ : input
530
+ ], { blockHash, isFallback: isEmpty && !!meta.fallback, isOptional: meta.modifier.isOptional, isPedantic: this.#isPedantic && !meta.modifier.isOptional });
531
+ } catch (error) {
532
+ throw new Error(`Unable to decode storage ${key.section || 'unknown'}.${key.method || 'unknown'}:${entryNum}: ${(error as Error).message}`);
533
+ }
534
+ }
535
+ }
@@ -0,0 +1,129 @@
1
+ // Copyright 2017-2025 @polkadot/rpc-core authors & contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /// <reference types="@pezkuwi/dev-test/globals.d.ts" />
5
+
6
+ import type { RpcInterface } from './types/index.js';
7
+
8
+ import { createTestPairs } from '@pezkuwi/keyring/testingPairs';
9
+ import { MockProvider } from '@pezkuwi/rpc-provider/mock';
10
+ import { TypeRegistry } from '@pezkuwi/types/create';
11
+
12
+ import { RpcCore } from './index.js';
13
+
14
+ describe('Cached Observables', (): void => {
15
+ const registry = new TypeRegistry();
16
+ let rpc: RpcCore & RpcInterface;
17
+ let provider: MockProvider;
18
+ const keyring = createTestPairs();
19
+
20
+ beforeEach((): void => {
21
+ provider = new MockProvider(registry);
22
+ rpc = new RpcCore('123', registry, { provider }) as (RpcCore & RpcInterface);
23
+ });
24
+
25
+ afterEach(async () => {
26
+ await provider.disconnect();
27
+ });
28
+
29
+ it('creates a single observable for subscriptions (multiple calls)', (): void => {
30
+ const observable1 = rpc.state.subscribeStorage([123]);
31
+ const observable2 = rpc.state.subscribeStorage([123]);
32
+
33
+ expect(observable2).toBe(observable1);
34
+ });
35
+
36
+ it('creates a single observable for subscriptions (multiple calls, no arguments)', (): void => {
37
+ const observable1 = rpc.chain.subscribeNewHeads();
38
+ const observable2 = rpc.chain.subscribeNewHeads();
39
+
40
+ expect(observable2).toBe(observable1);
41
+ });
42
+
43
+ it('creates a single observable (multiple calls, different arguments that should be cached together)', (): void => {
44
+ const observable1 = rpc.state.subscribeStorage([keyring.alice.address]);
45
+ const observable2 = rpc.state.subscribeStorage([registry.createType('AccountId', keyring.alice.address)]);
46
+
47
+ expect(observable2).toBe(observable1);
48
+ });
49
+
50
+ it('creates multiple observables for different values', (): void => {
51
+ const observable1 = rpc.chain.getBlockHash(123);
52
+ const observable2 = rpc.chain.getBlockHash(456);
53
+
54
+ expect(observable2).not.toBe(observable1);
55
+ });
56
+
57
+ it('subscribes to the same one if within the period (unbsub delay)', async (): Promise<void> => {
58
+ const observable1 = rpc.chain.subscribeNewHeads();
59
+ const sub1 = observable1.subscribe();
60
+
61
+ sub1.unsubscribe();
62
+
63
+ await new Promise<boolean>((resolve) => {
64
+ setTimeout((): void => {
65
+ const observable2 = rpc.chain.subscribeNewHeads();
66
+ const sub2 = observable2.subscribe();
67
+
68
+ expect(observable1).toBe(observable2);
69
+
70
+ sub2.unsubscribe();
71
+ resolve(true);
72
+ }, 500);
73
+ });
74
+ });
75
+
76
+ it('clears cache if there are no more subscribers', async (): Promise<void> => {
77
+ const observable1 = rpc.chain.subscribeNewHeads();
78
+ const observable2 = rpc.chain.subscribeNewHeads();
79
+ const sub1 = observable1.subscribe();
80
+ const sub2 = observable2.subscribe();
81
+
82
+ expect(observable1).toBe(observable2);
83
+
84
+ sub1.unsubscribe();
85
+ sub2.unsubscribe();
86
+
87
+ await new Promise<boolean>((resolve) => {
88
+ setTimeout((): void => {
89
+ // No more subscribers, now create a new observable
90
+ const observable3 = rpc.chain.subscribeNewHeads();
91
+
92
+ expect(observable3).not.toBe(observable1);
93
+ resolve(true);
94
+ }, 3500);
95
+ });
96
+ });
97
+
98
+ it('creates different observables for different methods but same arguments', (): void => {
99
+ // params do not match here
100
+ const observable1 = rpc.chain.getHeader('123');
101
+ const observable2 = rpc.chain.getBlockHash('123');
102
+
103
+ expect(observable2).not.toBe(observable1);
104
+ });
105
+
106
+ it('creates single observables for subsequent one-shots', (): void => {
107
+ const observable1 = rpc.chain.getBlockHash(123);
108
+ const observable2 = rpc.chain.getBlockHash(123);
109
+
110
+ expect(observable2).toBe(observable1);
111
+ });
112
+
113
+ it('creates multiple observables for subsequent one-shots delayed', async (): Promise<void> => {
114
+ const observable1 = rpc.chain.getBlockHash(123);
115
+
116
+ const sub = observable1.subscribe((): void => {
117
+ sub.unsubscribe();
118
+ });
119
+
120
+ expect(rpc.chain.getBlockHash(123)).toBe(observable1);
121
+
122
+ await new Promise<boolean>((resolve) => {
123
+ setTimeout((): void => {
124
+ expect(rpc.chain.getBlockHash(123)).not.toBe(observable1);
125
+ resolve(true);
126
+ }, 3500);
127
+ });
128
+ });
129
+ });
@@ -0,0 +1,4 @@
1
+ // Copyright 2017-2025 @polkadot/rpc-core authors & contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import '@pezkuwi/rpc-augment';