polkadot-js/api

View on GitHub
packages/api/src/promise/decorateMethod.ts

Summary

Maintainability
A
0 mins
Test Coverage
// Copyright 2017-2024 @polkadot/api authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { Observable, Subscription } from 'rxjs';
import type { Callback, Codec } from '@polkadot/types/types';
import type { DecorateFn, DecorateMethodOptions, ObsInnerType, StorageEntryPromiseOverloads, UnsubscribePromise, VoidFn } from '../types/index.js';

import { catchError, EMPTY, tap } from 'rxjs';

import { isFunction, nextTick } from '@polkadot/util';

interface Tracker<T> {
  reject: (value: Error) => Observable<never>;
  resolve: (value: T) => void;
}

type CodecReturnType<T extends (...args: unknown[]) => Observable<Codec>> =
  T extends (...args: any) => infer R
    ? R extends Observable<Codec>
      ? ObsInnerType<R>
      : never
    : never;

// a Promise completion tracker, wrapping an isComplete variable that ensures
// that the promise only resolves once
export function promiseTracker<T> (resolve: (value: T) => void, reject: (value: Error) => void): Tracker<T> {
  let isCompleted = false;

  return {
    reject: (error: Error): Observable<never> => {
      if (!isCompleted) {
        isCompleted = true;

        reject(error);
      }

      return EMPTY;
    },
    resolve: (value: T): void => {
      if (!isCompleted) {
        isCompleted = true;

        resolve(value);
      }
    }
  };
}

// extract the arguments and callback params from a value array possibly containing a callback
function extractArgs (args: unknown[], needsCallback: boolean): [unknown[], Callback<Codec> | undefined] {
  const actualArgs = args.slice();

  // If the last arg is a function, we pop it, put it into callback.
  // actualArgs will then hold the actual arguments to be passed to `method`
  const callback = (args.length && isFunction(args[args.length - 1]))
    ? actualArgs.pop() as Callback<Codec>
    : undefined;

  // When we need a subscription, ensure that a valid callback is actually passed
  if (needsCallback && !isFunction(callback)) {
    throw new Error('Expected a callback to be passed with subscriptions');
  }

  return [actualArgs, callback];
}

// Decorate a call for a single-shot result - retrieve and then immediate unsubscribe
function decorateCall<M extends DecorateFn<CodecReturnType<M>>> (method: M, args: unknown[]): Promise<CodecReturnType<M>> {
  return new Promise((resolve, reject): void => {
    // single result tracker - either reject with Error or resolve with Codec result
    const tracker = promiseTracker(resolve, reject);

    // encoding errors reject immediately, any result unsubscribes and resolves
    const subscription: Subscription = method(...args)
      .pipe(
        catchError((error: Error) => tracker.reject(error))
      )
      .subscribe((result): void => {
        tracker.resolve(result);

        nextTick(() => subscription.unsubscribe());
      });
  });
}

// Decorate a subscription where we have a result callback specified
function decorateSubscribe<M extends DecorateFn<CodecReturnType<M>>> (method: M, args: unknown[], resultCb: Callback<Codec>): UnsubscribePromise {
  return new Promise<VoidFn>((resolve, reject): void => {
    // either reject with error or resolve with unsubscribe callback
    const tracker = promiseTracker(resolve, reject);

    // errors reject immediately, the first result resolves with an unsubscribe promise, all results via callback
    const subscription: Subscription = method(...args)
      .pipe(
        catchError((error: Error) => tracker.reject(error)),
        tap(() => tracker.resolve(() => subscription.unsubscribe()))
      )
      .subscribe((result): void => {
        // queue result (back of queue to clear current)
        nextTick(() => resultCb(result));
      });
  });
}

/**
 * @description Decorate method for ApiPromise, where the results are converted to the Promise equivalent
 */
export function toPromiseMethod<M extends DecorateFn<CodecReturnType<M>>> (method: M, options?: DecorateMethodOptions): StorageEntryPromiseOverloads {
  const needsCallback = !!(options?.methodName && options.methodName.includes('subscribe'));

  return function (...args: unknown[]): Promise<CodecReturnType<M>> | UnsubscribePromise {
    const [actualArgs, resultCb] = extractArgs(args, needsCallback);

    return resultCb
      ? decorateSubscribe(method, actualArgs, resultCb)
      : decorateCall((options?.overrideNoSub as M) || method, actualArgs);
  } as StorageEntryPromiseOverloads;
}