@talismn/util 0.4.2 → 0.5.1

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.
@@ -0,0 +1,3 @@
1
+ export type Prettify<T> = {
2
+ [K in keyof T]: T[K];
3
+ } & {};
@@ -1 +1,2 @@
1
1
  export declare const classNames: (...classLists: import("tailwind-merge").ClassNameValue[]) => string;
2
+ export declare const cn: (...classLists: import("tailwind-merge").ClassNameValue[]) => string;
@@ -0,0 +1,25 @@
1
+ import { Observable } from "rxjs";
2
+ type LoadableError = {
3
+ name: string;
4
+ message: string;
5
+ };
6
+ export type Loadable<T = unknown> = {
7
+ status: "loading";
8
+ data?: T;
9
+ error?: undefined;
10
+ } | {
11
+ status: "success";
12
+ data: T;
13
+ error?: undefined;
14
+ } | {
15
+ status: "error";
16
+ data?: T;
17
+ error: LoadableError;
18
+ };
19
+ export type LoadableStatus = Loadable["status"];
20
+ export type LoadableOptions = {
21
+ getError?: (error: unknown) => LoadableError;
22
+ refreshInterval?: number;
23
+ };
24
+ export declare function getLoadable$<T>(factory: () => Promise<T>, options?: LoadableOptions): Observable<Loadable<T>>;
25
+ export {};
@@ -0,0 +1,13 @@
1
+ import { Observable } from "rxjs";
2
+ /**
3
+ * When using react-rxjs hooks and state observables, the options are used as weak map keys.
4
+ * This means that if the options object is recreated on each render, the observable will be recreated as well.
5
+ * This utility function allows you to create a shared observable based on a namespace and arguments that, so react-rxjs can reuse the same observables
6
+ *
7
+ * @param namespace
8
+ * @param args
9
+ * @param createObservable
10
+ * @param serializer
11
+ * @returns
12
+ */
13
+ export declare const getSharedObservable: <Args, Output, ObsOutput = Observable<Output>>(namespace: string, args: Args, createObservable: (args: Args) => ObsOutput, serializer?: (args: Args) => string) => ObsOutput;
@@ -1,30 +1,30 @@
1
1
  export * from "./addTrailingSlash";
2
2
  export * from "./BigMath";
3
- export * from "./blake2Concat";
4
3
  export * from "./classNames";
5
- export * from "./convertAddress";
6
- export * from "./decodeAnyAddress";
7
- export * from "./decodeSs58Format";
8
4
  export * from "./deferred";
9
- export * from "./encodeAnyAddress";
10
5
  export * from "./firstThenDebounce";
11
6
  export * from "./formatDecimals";
12
7
  export * from "./formatPrice";
13
8
  export * from "./FunctionPropertyNames";
9
+ export * from "./getSharedObservable";
14
10
  export * from "./hasOwnProperty";
11
+ export * from "./isAbortError";
15
12
  export * from "./isArrayOf";
13
+ export * from "./isAscii";
16
14
  export * from "./isBigInt";
17
15
  export * from "./isBooleanTrue";
18
- export * from "./isEthereumAddress";
19
- export * from "./isValidSubstrateAddress";
16
+ export * from "./isHexString";
17
+ export * from "./isNotNil";
18
+ export * from "./isPromise";
19
+ export * from "./isSubject";
20
+ export * from "./isTruthy";
21
+ export * from "./keepAlive";
20
22
  export * from "./planckToTokens";
23
+ export * from "./Prettify";
24
+ export * from "./replaySubjectFrom";
21
25
  export * from "./sleep";
26
+ export * from "./splitSubject";
22
27
  export * from "./throwAfter";
23
28
  export * from "./tokensToPlanck";
24
- export * from "./twox64Concat";
25
29
  export * from "./validateHexString";
26
- export * from "./normalizeAddress";
27
- export * from "./isAddressEqual";
28
- export * from "./isAscii";
29
- export * from "./isNotNil";
30
- export * from "./isTruthy";
30
+ export * from "./getLoadable";
@@ -0,0 +1 @@
1
+ export declare const isAbortError: (error: unknown) => boolean;
@@ -0,0 +1,3 @@
1
+ export type HexString = `0x${string}`;
2
+ export declare const REGEX_HEX_STRING: RegExp;
3
+ export declare const isHexString: (value: unknown) => value is HexString;
@@ -0,0 +1 @@
1
+ export declare const isPromise: <T = any>(value: any) => value is Promise<T>;
@@ -0,0 +1,5 @@
1
+ import { Subject } from "rxjs";
2
+ /**
3
+ * Tests to see if an object is an RxJS {@link Subject}.
4
+ */
5
+ export declare function isSubject<T>(object?: Subject<T> | object): object is Subject<T>;
@@ -0,0 +1,17 @@
1
+ import type { OperatorFunction } from "rxjs";
2
+ /**
3
+ * An RxJS operator that keeps the source observable alive for a specified duration
4
+ * after all subscribers have unsubscribed. This prevents expensive re-subscriptions
5
+ * when subscribers come and go frequently.
6
+ *
7
+ * @param keepAliveMs - Duration in milliseconds to keep the source alive after last unsubscription
8
+ * @returns MonoTypeOperatorFunction that can be used in pipe()
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * const data$ = expensive_api_call$.pipe(
13
+ * keepAlive(3000) // Keep alive for 3 seconds
14
+ * );
15
+ * ```
16
+ */
17
+ export declare const keepAlive: <T>(timeout: number) => OperatorFunction<T, T>;
@@ -0,0 +1,12 @@
1
+ import { ReplaySubject } from "rxjs";
2
+ /**
3
+ * Turns a value into a {@link ReplaySubject} of size 1.
4
+ *
5
+ * If the value is already a {@link ReplaySubject}, it will be returned as-is.
6
+ *
7
+ * If the value is a {@link Promise}, it will be awaited,
8
+ * and the awaited value will be published into the {@link ReplaySubject} when it becomes available.
9
+ *
10
+ * For any other type of value, it will be immediately published into the {@link ReplaySubject}.
11
+ */
12
+ export declare const replaySubjectFrom: <T>(initialValue: T | Promise<T> | ReplaySubject<T>) => ReplaySubject<T>;
@@ -0,0 +1,11 @@
1
+ import { Observable, Subject } from "rxjs";
2
+ /**
3
+ * Takes a subject and splits it into two parts:
4
+ *
5
+ * 1. A function to submit new values into the subject.
6
+ * 2. An observable for subscribing to new values from the subject.
7
+ *
8
+ * This can be helpful when, to avoid bugs, you want to expose only one
9
+ * of these parts to external code and keep the other part private.
10
+ */
11
+ export declare function splitSubject<T>(subject: Subject<T>): readonly [(value: T) => void, Observable<T>];
@@ -1,12 +1,11 @@
1
- import { HexString } from "@polkadot/util/types";
2
1
  /**
3
2
  * @name validateHexString
4
3
  * @description Checks if a string is a hex string. Required to account for type differences between different polkadot libraries
5
4
  * @param {string} str - string to check
6
- * @returns {HexString} - boolean
5
+ * @returns {`0x${string}`} - boolean
7
6
  * @example
8
7
  * validateHexString("0x1234") // "0x1234"
9
8
  * validateHexString("1234") // Error: Expected a hex string
10
9
  * validateHexString(1234) // Error: Expected a string
11
10
  **/
12
- export declare const validateHexString: (str: string) => HexString;
11
+ export declare const validateHexString: (str: string) => `0x${string}`;
@@ -1,9 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var util = require('@polkadot/util');
4
- var utilCrypto = require('@polkadot/util-crypto');
5
3
  var tailwindMerge = require('tailwind-merge');
6
- var keyring = require('@polkadot/keyring');
7
4
  var rxjs = require('rxjs');
8
5
  var BigNumber = require('bignumber.js');
9
6
 
@@ -45,54 +42,8 @@ const BigMath = {
45
42
  }
46
43
  };
47
44
 
48
- const bitLength$1 = 128;
49
- function blake2Concat(input) {
50
- return util.u8aToHex(util.u8aConcat(utilCrypto.blake2AsU8a(input, bitLength$1), util.u8aToU8a(input)));
51
- }
52
-
53
45
  const classNames = tailwindMerge.twMerge;
54
-
55
- function encodeAnyAddress(key, ss58Format) {
56
- try {
57
- return keyring.encodeAddress(key, ss58Format);
58
- } catch (error) {
59
- if (typeof key !== "string") throw error;
60
- if (!utilCrypto.isEthereumAddress(key)) throw error;
61
- return utilCrypto.ethereumEncode(key);
62
- }
63
- }
64
-
65
- /**
66
- *
67
- * @param address substrate SS58 address
68
- * @param prefix prefix used to format the address
69
- * @returns address encoded with supplied prefix
70
- */
71
- const convertAddress = (address, prefix) => {
72
- return encodeAnyAddress(address, prefix ?? undefined);
73
- };
74
-
75
- function decodeAnyAddress(encoded, ignoreChecksum, ss58Format) {
76
- try {
77
- return keyring.decodeAddress(encoded, ignoreChecksum, ss58Format);
78
- } catch (error) {
79
- if (typeof encoded !== "string") throw error;
80
- if (!utilCrypto.isEthereumAddress(encoded)) throw error;
81
- return util.hexToU8a(encoded.slice("0x".length));
82
- }
83
- }
84
-
85
- const decodeSs58Format = address => {
86
- if (!address) return;
87
- try {
88
- utilCrypto.decodeAddress(address);
89
- const decoded = utilCrypto.base58Decode(address);
90
- const [,,, ss58Format] = utilCrypto.checkAddressChecksum(decoded);
91
- return ss58Format;
92
- } catch {
93
- return; // invalid address
94
- }
95
- };
46
+ const cn = tailwindMerge.twMerge;
96
47
 
97
48
  /**
98
49
  * In TypeScript, a deferred promise refers to a pattern that involves creating a promise that can be
@@ -195,43 +146,129 @@ const formatPrice = (price, currency, compact) => {
195
146
  }).format(price);
196
147
  };
197
148
 
149
+ const CACHE = new Map();
150
+
151
+ /**
152
+ * When using react-rxjs hooks and state observables, the options are used as weak map keys.
153
+ * This means that if the options object is recreated on each render, the observable will be recreated as well.
154
+ * This utility function allows you to create a shared observable based on a namespace and arguments that, so react-rxjs can reuse the same observables
155
+ *
156
+ * @param namespace
157
+ * @param args
158
+ * @param createObservable
159
+ * @param serializer
160
+ * @returns
161
+ */
162
+ const getSharedObservable = (namespace, args, createObservable, serializer = args => JSON.stringify(args)) => {
163
+ const cacheKey = `${namespace}:${serializer(args)}`;
164
+ if (CACHE.has(cacheKey)) return CACHE.get(cacheKey);
165
+ const obs = createObservable(args);
166
+ const sharedObs = obs.pipe(rxjs.shareReplay({
167
+ bufferSize: 1,
168
+ refCount: true
169
+ }));
170
+ CACHE.set(cacheKey, sharedObs);
171
+ return sharedObs;
172
+ };
173
+
198
174
  function hasOwnProperty(obj, prop) {
199
175
  if (typeof obj !== "object") return false;
200
176
  if (obj === null) return false;
201
177
  return prop in obj;
202
178
  }
203
179
 
180
+ const isAbortError = error => {
181
+ return error instanceof Error && error.name === "AbortError";
182
+ };
183
+
204
184
  function isArrayOf(array, func) {
205
185
  if (array.length > 0 && array[0] instanceof func) return true;
206
186
  return false;
207
187
  }
208
188
 
189
+ const isAscii = str => {
190
+ return [...str].every(char => char.charCodeAt(0) <= 127);
191
+ };
192
+
209
193
  const isBigInt = value => typeof value === "bigint";
210
194
 
211
195
  const isBooleanTrue = x => !!x;
212
196
 
213
- const isEthereumAddress = address => !!address && address.startsWith("0x") && address.length === 42;
197
+ const REGEX_HEX_STRING = /^0x[0-9a-fA-F]*$/;
198
+ const isHexString = value => {
199
+ return typeof value === "string" && REGEX_HEX_STRING.test(value);
200
+ };
214
201
 
215
202
  /**
216
- * Similar to isValidAddress but will not call isEthereumAddress under the hood
217
- * @param address
218
- * @param prefix if supplied, the method will also check the prefix
219
- * @returns true if valid substrate (SS58) address, false otherwise
203
+ * WARNING: This function only checks against null or undefined, it does not coerce the value.
204
+ * ie: false and 0 are considered not nil
205
+ * Use isTruthy instead for a regular coercion check.
206
+ *
207
+ * @param value
208
+ * @returns whether the value is neither null nor undefined
220
209
  */
221
- const isValidSubstrateAddress = (address, prefix) => {
222
- try {
223
- // attempt to encode, it will throw an error if address is invalid
224
- const encoded = keyring.encodeAddress(address, prefix ?? undefined);
225
-
226
- //if a prefix is supplied, check that reencoding using this prefix matches the input address
227
- if (prefix !== undefined) return address === encoded;
228
-
229
- //if no prefix supplied, the fact that decoding + encoding succeded indicates that the address is valid
230
- return true;
231
- } catch (error) {
232
- // input is not a substrate (SS58) address
233
- return false;
234
- }
210
+ const isNotNil = value => value !== null && value !== undefined;
211
+
212
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
213
+ const isPromise = value => !!value && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
214
+
215
+ /**
216
+ * Tests to see if an object is an RxJS {@link Subject}.
217
+ */
218
+ function isSubject(object) {
219
+ if (!object) return false;
220
+ if (object instanceof rxjs.Subject) return true;
221
+ return "asObservable" in object && isFn(object.asObservable) && "complete" in object && isFn(object.complete) && "error" in object && isFn(object.error) && "forEach" in object && isFn(object.forEach) && "next" in object && isFn(object.next) && "pipe" in object && isFn(object.pipe) && "subscribe" in object && isFn(object.subscribe) && "unsubscribe" in object && isFn(object.unsubscribe) && "closed" in object && isBool(object.closed) && "observed" in object && isBool(object.observed);
222
+ }
223
+
224
+ /**
225
+ * Returns `true` if `value` is a function.
226
+ */
227
+ function isFn(value) {
228
+ return typeof value === "function";
229
+ }
230
+
231
+ /**
232
+ * Returns `true` if `value` is a boolean.
233
+ */
234
+ function isBool(value) {
235
+ return typeof value === "boolean";
236
+ }
237
+
238
+ const isTruthy = value => Boolean(value);
239
+
240
+ /**
241
+ * An RxJS operator that keeps the source observable alive for a specified duration
242
+ * after all subscribers have unsubscribed. This prevents expensive re-subscriptions
243
+ * when subscribers come and go frequently.
244
+ *
245
+ * @param keepAliveMs - Duration in milliseconds to keep the source alive after last unsubscription
246
+ * @returns MonoTypeOperatorFunction that can be used in pipe()
247
+ *
248
+ * @example
249
+ * ```typescript
250
+ * const data$ = expensive_api_call$.pipe(
251
+ * keepAlive(3000) // Keep alive for 3 seconds
252
+ * );
253
+ * ```
254
+ */
255
+ const keepAlive = timeout => {
256
+ let release;
257
+ return source => source.pipe(rxjs.tap({
258
+ subscribe: () => {
259
+ release = keepSourceSubscribed(source, timeout);
260
+ },
261
+ unsubscribe: () => {
262
+ release?.();
263
+ }
264
+ }), rxjs.shareReplay({
265
+ refCount: true,
266
+ bufferSize: 1
267
+ }));
268
+ };
269
+ const keepSourceSubscribed = (observable, ms) => {
270
+ const sub = observable.subscribe();
271
+ return () => setTimeout(() => sub.unsubscribe(), ms);
235
272
  };
236
273
 
237
274
  function planckToTokens(planck, tokenDecimals) {
@@ -242,10 +279,50 @@ function planckToTokens(planck, tokenDecimals) {
242
279
  return new BigNumber__default.default(planck).multipliedBy(multiplier).toString(10);
243
280
  }
244
281
 
282
+ /**
283
+ * Turns a value into a {@link ReplaySubject} of size 1.
284
+ *
285
+ * If the value is already a {@link ReplaySubject}, it will be returned as-is.
286
+ *
287
+ * If the value is a {@link Promise}, it will be awaited,
288
+ * and the awaited value will be published into the {@link ReplaySubject} when it becomes available.
289
+ *
290
+ * For any other type of value, it will be immediately published into the {@link ReplaySubject}.
291
+ */
292
+ const replaySubjectFrom = initialValue => {
293
+ if (initialValue instanceof rxjs.ReplaySubject) return initialValue;
294
+ const subject = new rxjs.ReplaySubject(1);
295
+
296
+ // if initialValue is a promise, await it and then call `subject.next()` with the awaited value
297
+ if (isPromise(initialValue)) {
298
+ initialValue.then(value => subject.next(value), error => subject.error(error));
299
+ return subject;
300
+ }
301
+
302
+ // if initialValue is not a promise, immediately call `subject.next()` with the value
303
+ subject.next(initialValue);
304
+ return subject;
305
+ };
306
+
245
307
  const sleep = ms => new Promise(resolve => {
246
308
  if (process.env.NODE_ENV === "test") resolve();else setTimeout(resolve, ms);
247
309
  });
248
310
 
311
+ /**
312
+ * Takes a subject and splits it into two parts:
313
+ *
314
+ * 1. A function to submit new values into the subject.
315
+ * 2. An observable for subscribing to new values from the subject.
316
+ *
317
+ * This can be helpful when, to avoid bugs, you want to expose only one
318
+ * of these parts to external code and keep the other part private.
319
+ */
320
+ function splitSubject(subject) {
321
+ const next = value => subject.next(value);
322
+ const observable = subject.asObservable();
323
+ return [next, observable];
324
+ }
325
+
249
326
  const throwAfter = (ms, reason) => new Promise((_, reject) => setTimeout(() => reject(new Error(reason)), ms));
250
327
 
251
328
  function tokensToPlanck(tokens, tokenDecimals) {
@@ -256,17 +333,11 @@ function tokensToPlanck(tokens, tokenDecimals) {
256
333
  return new BigNumber__default.default(tokens).multipliedBy(multiplier).toString(10);
257
334
  }
258
335
 
259
- const bitLength = 64;
260
- function twox64Concat(input) {
261
- const inputAsU8a = typeof input === "string" ? input : new Uint8Array(input);
262
- return util.u8aToHex(util.u8aConcat(utilCrypto.xxhashAsU8a(inputAsU8a, bitLength), util.u8aToU8a(inputAsU8a)));
263
- }
264
-
265
336
  /**
266
337
  * @name validateHexString
267
338
  * @description Checks if a string is a hex string. Required to account for type differences between different polkadot libraries
268
339
  * @param {string} str - string to check
269
- * @returns {HexString} - boolean
340
+ * @returns {`0x${string}`} - boolean
270
341
  * @example
271
342
  * validateHexString("0x1234") // "0x1234"
272
343
  * validateHexString("1234") // Error: Expected a hex string
@@ -282,67 +353,68 @@ const validateHexString = str => {
282
353
  throw new Error("Expected a hex string");
283
354
  };
284
355
 
285
- const CACHE = new Map();
286
-
287
- // Normalize an address in a way that it can be compared to other addresses that have also been normalized
288
- const normalizeAddress = address => {
289
- try {
290
- if (!CACHE.has(address)) CACHE.set(address, encodeAnyAddress(address));
291
- return CACHE.get(address);
292
- } catch (cause) {
293
- throw new Error(`Unable to normalize address: ${address}`, {
294
- cause
295
- });
356
+ // Designed to be serializable as it can be sent to the frontend
357
+
358
+ function getLoadable$(factory, options = {}) {
359
+ const {
360
+ getError,
361
+ refreshInterval
362
+ } = options;
363
+ const createLoadableStream = () => rxjs.from(factory()).pipe(rxjs.map(data => ({
364
+ status: "success",
365
+ data
366
+ })), rxjs.catchError(error => rxjs.of({
367
+ status: "error",
368
+ error: getError ? getError(error) : getGenericError(error)
369
+ })));
370
+ const source$ = refreshInterval ? rxjs.timer(0, refreshInterval).pipe(rxjs.switchMap(() => createLoadableStream())) : createLoadableStream();
371
+ return source$.pipe(rxjs.startWith({
372
+ status: "loading"
373
+ }));
374
+ }
375
+ const getGenericError = error => ({
376
+ name: "Error",
377
+ message: getGenericErrorMessage(error)
378
+ });
379
+ const getGenericErrorMessage = error => {
380
+ if (typeof error === "string") {
381
+ return error;
382
+ } else if (error instanceof Error) {
383
+ return error.message;
384
+ } else if (error && typeof error === "object" && "message" in error) {
385
+ return error.message;
296
386
  }
387
+ return String(error) || "Unknown error";
297
388
  };
298
389
 
299
- const isAddressEqual = (address1, address2) => {
300
- return normalizeAddress(address1) === normalizeAddress(address2);
301
- };
302
-
303
- const isAscii = str => {
304
- return [...str].every(char => char.charCodeAt(0) <= 127);
305
- };
306
-
307
- /**
308
- * WARNING: This function only checks against null or undefined, it does not coerce the value.
309
- * ie: false and 0 are considered not nil
310
- * Use isTruthy instead for a regular coercion check.
311
- *
312
- * @param value
313
- * @returns whether the value is neither null nor undefined
314
- */
315
- const isNotNil = value => value !== null && value !== undefined;
316
-
317
- const isTruthy = value => Boolean(value);
318
-
319
390
  exports.BigMath = BigMath;
320
391
  exports.Deferred = Deferred;
321
392
  exports.MAX_DECIMALS_FORMAT = MAX_DECIMALS_FORMAT;
393
+ exports.REGEX_HEX_STRING = REGEX_HEX_STRING;
322
394
  exports.addTrailingSlash = addTrailingSlash;
323
- exports.blake2Concat = blake2Concat;
324
395
  exports.classNames = classNames;
325
- exports.convertAddress = convertAddress;
326
- exports.decodeAnyAddress = decodeAnyAddress;
327
- exports.decodeSs58Format = decodeSs58Format;
328
- exports.encodeAnyAddress = encodeAnyAddress;
396
+ exports.cn = cn;
329
397
  exports.firstThenDebounce = firstThenDebounce;
330
398
  exports.formatDecimals = formatDecimals;
331
399
  exports.formatPrice = formatPrice;
400
+ exports.getLoadable$ = getLoadable$;
401
+ exports.getSharedObservable = getSharedObservable;
332
402
  exports.hasOwnProperty = hasOwnProperty;
333
- exports.isAddressEqual = isAddressEqual;
403
+ exports.isAbortError = isAbortError;
334
404
  exports.isArrayOf = isArrayOf;
335
405
  exports.isAscii = isAscii;
336
406
  exports.isBigInt = isBigInt;
337
407
  exports.isBooleanTrue = isBooleanTrue;
338
- exports.isEthereumAddress = isEthereumAddress;
408
+ exports.isHexString = isHexString;
339
409
  exports.isNotNil = isNotNil;
410
+ exports.isPromise = isPromise;
411
+ exports.isSubject = isSubject;
340
412
  exports.isTruthy = isTruthy;
341
- exports.isValidSubstrateAddress = isValidSubstrateAddress;
342
- exports.normalizeAddress = normalizeAddress;
413
+ exports.keepAlive = keepAlive;
343
414
  exports.planckToTokens = planckToTokens;
415
+ exports.replaySubjectFrom = replaySubjectFrom;
344
416
  exports.sleep = sleep;
417
+ exports.splitSubject = splitSubject;
345
418
  exports.throwAfter = throwAfter;
346
419
  exports.tokensToPlanck = tokensToPlanck;
347
- exports.twox64Concat = twox64Concat;
348
420
  exports.validateHexString = validateHexString;