@keeex/utils 7.5.0 → 7.6.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.
package/lib/array.d.ts CHANGED
@@ -13,6 +13,7 @@
13
13
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
14
14
  *
15
15
  */
16
+ import type { Arrayable } from "./types/array.js";
16
17
  export type EqualityFunction<T1, T2> = (op1: T1, op2: T2) => boolean;
17
18
  /**
18
19
  * Compare two array-like structure for equal content
@@ -25,4 +26,6 @@ export declare const arrayEqual: <T1 = unknown, T2 = unknown>(op1: ArrayLike<T1>
25
26
  *
26
27
  * @public
27
28
  */
28
- export declare const asArray: <T>(data: Array<T> | (T extends Array<unknown> ? never : T)) => Array<T>;
29
+ export declare const asArray: <T>(data: Arrayable<T>) => Array<T>;
30
+ /** Return a new array with its string sorted */
31
+ export declare const toSortedStringArray: (array: Array<string>) => Array<string>;
package/lib/array.js CHANGED
@@ -13,6 +13,7 @@
13
13
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
14
14
  *
15
15
  */
16
+ import { alphaSort } from "./string.js";
16
17
  const useEqOp = (op1, op2) => op1 === op2;
17
18
  /**
18
19
  * Compare two array-like structure for equal content
@@ -33,4 +34,6 @@ export const arrayEqual = (op1, op2, eqFunc = useEqOp) => {
33
34
  *
34
35
  * @public
35
36
  */
36
- export const asArray = (data) => Array.isArray(data) ? data : [data];
37
+ export const asArray = (data) => (Array.isArray(data) ? data : [data]);
38
+ /** Return a new array with its string sorted */
39
+ export const toSortedStringArray = (array) => array.toSorted(alphaSort);
@@ -17,6 +17,8 @@
17
17
  * Allows returning a promise and resolving it in another scope
18
18
  *
19
19
  * @public
20
+ *
21
+ * @deprecated Use `Promise.withResolvers()`
20
22
  */
21
23
  export declare class DeferredPromise<ResultType> {
22
24
  #private;
@@ -19,6 +19,8 @@
19
19
  * Allows returning a promise and resolving it in another scope
20
20
  *
21
21
  * @public
22
+ *
23
+ * @deprecated Use `Promise.withResolvers()`
22
24
  */
23
25
  export class DeferredPromise {
24
26
  /** The actual promise that can be awaited */
package/lib/bits/hex.d.ts CHANGED
@@ -13,5 +13,12 @@
13
13
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
14
14
  *
15
15
  */
16
- /** Clean an hex string input; make it even and remove 0x prefix if present */
16
+ /** Remove the 0x prefix if present. */
17
+ export declare const getHexNoPrefix: (input: string) => string;
18
+ /** Ensure that the input is a valid hexadecimal string (only allow `[0-9a-fA-F]`) */
19
+ export declare const isHexString: (input: string) => boolean;
20
+ /**
21
+ * Clean an hex string input; make it even and remove 0x prefix if present.
22
+ * Also convert it to lowercase and check if the input is actually an hexadecimal string.
23
+ */
17
24
  export declare const clearHexInput: (input: string) => string;
package/lib/bits/hex.js CHANGED
@@ -16,15 +16,33 @@
16
16
  const CHARACTER_PER_BYTE = 2;
17
17
  const HEX_PREFIX = "0x";
18
18
  const HEX_PREFIX_LENGTH = HEX_PREFIX.length;
19
- /** Clean an hex string input; make it even and remove 0x prefix if present */
19
+ /** Remove the 0x prefix if present. */
20
+ export const getHexNoPrefix = (input) => input.startsWith(HEX_PREFIX) ? input.substring(HEX_PREFIX_LENGTH) : input;
21
+ const HEX_RANGES_NUMBERS = { min: "0".charCodeAt(0), max: "9".charCodeAt(0) };
22
+ const HEX_RANGES_LOWERCASE = { min: "a".charCodeAt(0), max: "f".charCodeAt(0) };
23
+ const HEX_RANGES_UPPERCASE = { min: "A".charCodeAt(0), max: "F".charCodeAt(0) };
24
+ /** Ensure that the input is a valid hexadecimal string (only allow `[0-9a-fA-F]`) */
25
+ export const isHexString = (input) => {
26
+ for (let i = 0; i < input.length; ++i) {
27
+ const charCode = input.charCodeAt(i);
28
+ if ((charCode < HEX_RANGES_NUMBERS.min || charCode > HEX_RANGES_NUMBERS.max) &&
29
+ (charCode < HEX_RANGES_LOWERCASE.min || charCode > HEX_RANGES_LOWERCASE.max) &&
30
+ (charCode < HEX_RANGES_UPPERCASE.min || charCode > HEX_RANGES_UPPERCASE.max)) {
31
+ return false;
32
+ }
33
+ }
34
+ return true;
35
+ };
36
+ /**
37
+ * Clean an hex string input; make it even and remove 0x prefix if present.
38
+ * Also convert it to lowercase and check if the input is actually an hexadecimal string.
39
+ */
20
40
  export const clearHexInput = (input) => {
21
- const noprefix = !input.startsWith(HEX_PREFIX);
41
+ const withoutPrefix = getHexNoPrefix(input);
42
+ if (!isHexString(withoutPrefix))
43
+ throw new Error("Input is not a valid hexadecimal string");
22
44
  const even = input.length % CHARACTER_PER_BYTE === 0;
23
- if (noprefix && even)
24
- return input;
25
- if (noprefix)
26
- return `0${input}`;
27
45
  if (even)
28
- return input.substring(HEX_PREFIX_LENGTH);
29
- return `0${input.substring(HEX_PREFIX_LENGTH)}`;
46
+ return withoutPrefix.toLowerCase();
47
+ return `0${withoutPrefix.toLowerCase()}`;
30
48
  };
package/lib/dict.d.ts CHANGED
@@ -54,3 +54,35 @@ export type PrimitiveTypeObject = Record<string, unknown> | Array<unknown> | str
54
54
  * @public
55
55
  */
56
56
  export declare const deepCopyPrimitive: (source: unknown) => unknown;
57
+ /**
58
+ * Perform the merge operation between two values.
59
+ *
60
+ * If the new value have no changes from the initial value, this should always return `initial`.
61
+ * If there is any change, this should always return a new reference.
62
+ *
63
+ * TODO: The function returns unknown because I can't work around the requirements for TypeScript to
64
+ * be happy using `DataType | null`.
65
+ */
66
+ type RefMergeDefFn<DataType> = (initial: DataType | null, newValue: DataType | null) => unknown;
67
+ type RefMergeDef<DataType> = DataType extends string ? "string" : DataType extends number ? "number" : DataType extends boolean ? "boolean" : DataType extends Array<string> ? "string[]" : RefMergeDefFn<DataType>;
68
+ type RefMergeProfile<ObjectType> = Required<{
69
+ [key in keyof ObjectType]: RefMergeDef<NonNullable<ObjectType[key]>>;
70
+ }>;
71
+ /**
72
+ * Examine each props on both `initial` and `newValue`, and if there is any change, return a new
73
+ * reference with all values.
74
+ *
75
+ * @param initial - The source object. If no changes are detected, returns it.
76
+ * @param newValue - Values to update in `initial`. `undefined` values are skipped.
77
+ * @param profile - Describe the expected properties and how to merge them.
78
+ *
79
+ * @returns
80
+ * If there is some change from `initial`, will return a new reference with all new values.
81
+ * This property extends to values that are object; all objects in this hierarchy that have an
82
+ * update belows it is replaced with a new reference.
83
+ *
84
+ * This is made to help with libraries like React that depends on state reference actually getting
85
+ * changed on update.
86
+ */
87
+ export declare const refMergeObjects: <ObjectType>(initial: ObjectType, newValue: Partial<ObjectType>, profile: RefMergeProfile<ObjectType>) => ObjectType;
88
+ export {};
package/lib/dict.js CHANGED
@@ -13,6 +13,7 @@
13
13
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
14
14
  *
15
15
  */
16
+ import { arrayEqual } from "./array.js";
16
17
  /**
17
18
  * Copy all properties from secondary into primary.
18
19
  *
@@ -72,3 +73,65 @@ export const deepCopyPrimitive = (source) => {
72
73
  return res;
73
74
  }
74
75
  };
76
+ const refMergeStringArray = (initial, newValue) => {
77
+ if (newValue === null)
78
+ return null;
79
+ if (initial !== null && arrayEqual(initial, newValue))
80
+ return initial;
81
+ return [...newValue];
82
+ };
83
+ /**
84
+ * Merge a new value and an old value according to the provided merger.
85
+ *
86
+ * If `newValue` is undefined, `initial` is always returned.
87
+ * If `newValue` is defined AND have difference from `initial`, a new object should be returned.
88
+ * If there is no change, `initial` is returned.
89
+ */
90
+ const getMergedRef = (initial, newValue, merger) => {
91
+ if (newValue === undefined)
92
+ return initial;
93
+ switch (merger) {
94
+ case "string":
95
+ case "number":
96
+ case "boolean":
97
+ return newValue;
98
+ case "string[]":
99
+ return refMergeStringArray(initial, newValue);
100
+ default: {
101
+ return merger(initial, newValue);
102
+ }
103
+ }
104
+ };
105
+ /**
106
+ * Examine each props on both `initial` and `newValue`, and if there is any change, return a new
107
+ * reference with all values.
108
+ *
109
+ * @param initial - The source object. If no changes are detected, returns it.
110
+ * @param newValue - Values to update in `initial`. `undefined` values are skipped.
111
+ * @param profile - Describe the expected properties and how to merge them.
112
+ *
113
+ * @returns
114
+ * If there is some change from `initial`, will return a new reference with all new values.
115
+ * This property extends to values that are object; all objects in this hierarchy that have an
116
+ * update belows it is replaced with a new reference.
117
+ *
118
+ * This is made to help with libraries like React that depends on state reference actually getting
119
+ * changed on update.
120
+ */
121
+ export const refMergeObjects = (initial, newValue, profile) => {
122
+ let anyChange = false;
123
+ const newValues = Object.entries(profile).map(([key, merger]) => {
124
+ const initialValue = initial[key];
125
+ const merged = getMergedRef(initialValue, newValue[key], merger);
126
+ anyChange ||= merged !== initialValue;
127
+ return { key, merged };
128
+ });
129
+ // Can't statically detect changes in map() callback
130
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
131
+ if (!anyChange)
132
+ return initial;
133
+ return newValues.reduce((acc, { key, merged }) => {
134
+ acc[key] = merged;
135
+ return acc;
136
+ }, {});
137
+ };
package/lib/string.d.ts CHANGED
@@ -63,3 +63,11 @@ export declare const encodeUTF8: (str: string) => string;
63
63
  export declare const decodeUTF8: (str: string, skipErrors?: boolean) => string;
64
64
  /** Capitalize (or uncapitalize) the first character of a string */
65
65
  export declare const capitalize: (str: string, upperFirstLetter: boolean) => string;
66
+ /** Ensure the string ends with one newline */
67
+ export declare const enforceNewlineEnd: (str: string) => string;
68
+ /** Format the text as a single line or multiline JS comment */
69
+ export declare const jsComment: (text: string, jsDoc?: boolean) => string;
70
+ /** Make sure the string starts with a capital, and all other letters are lowercase */
71
+ export declare const uncapitalize: (str: string) => string;
72
+ /** Helper to use with `Array.sort()` and similar */
73
+ export declare const alphaSort: (a: string, b: string) => number;
package/lib/string.js CHANGED
@@ -106,3 +106,35 @@ export const capitalize = (str, upperFirstLetter) => {
106
106
  const rest = str.slice(1);
107
107
  return `${transformedFirstLetter}${rest}`;
108
108
  };
109
+ /** Return the last index of a character that pass the needle test, or -1 if it never happens */
110
+ const findLastIndexOf = (hay, needle) => {
111
+ for (let index = hay.length - 1; --index; index >= 0) {
112
+ if (needle(hay.at(index)))
113
+ return index;
114
+ }
115
+ return -1;
116
+ };
117
+ /** Ensure the string ends with one newline */
118
+ export const enforceNewlineEnd = (str) => {
119
+ if (str.endsWith("\n")) {
120
+ const firstNonNewline = findLastIndexOf(str, (c) => c !== "\n");
121
+ if (firstNonNewline === -1)
122
+ return "\n";
123
+ return `${str.slice(0, firstNonNewline + 1)}\n`;
124
+ }
125
+ return `${str}\n`;
126
+ };
127
+ /** Format the text as a single line or multiline JS comment */
128
+ export const jsComment = (text, jsDoc = false) => {
129
+ const lines = text.split("\n");
130
+ if (lines.length === 1 && !jsDoc)
131
+ return `// ${lines[0]}`;
132
+ const prefix = jsDoc ? "/**" : "/*";
133
+ if (lines.length === 1)
134
+ return `${prefix} ${lines[0].replaceAll("*/", "* /")} */`;
135
+ return [prefix, ...lines.map((c) => ` * ${c.trimEnd().replaceAll("*/", "* /")}`), " */"].join("\n");
136
+ };
137
+ /** Make sure the string starts with a capital, and all other letters are lowercase */
138
+ export const uncapitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
139
+ /** Helper to use with `Array.sort()` and similar */
140
+ export const alphaSort = (a, b) => a.localeCompare(b);
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"@keeex/utils","version":"7.5.0","description":"Various utility functions for pure JavaScript","scripts":{},"author":"KeeeX SAS","contributors":[{"name":"Gabriel Paul \"Cley Faye\" Risterucci","email":"gabriel@keeex.net"}],"homepage":"https://keeex.me/oss","keywords":["utility"],"type":"module","license":"MIT","dependencies":{"@keeex/bubble_babble":"^3.0.1","@keeex/log":"^1.7.2","base64-arraybuffer":"^1.0.2","cron-parser":"^5.4.0","ms":"^2.1.3","text-encoding-shim":"^1.0.5"},"exports":{"./array.js":{"node":"./lib/array.js","browser":"./web/array.js","react-native":"./web/array.js","default":"./lib/array.js"},"./arraybuffer.js":{"node":"./lib/arraybuffer.js","browser":"./web/arraybuffer.js","react-native":"./web/arraybuffer.js","default":"./lib/arraybuffer.js"},"./async/asynctrigger.js":{"node":"./lib/async/asynctrigger.js","browser":"./web/async/asynctrigger.js","react-native":"./web/async/asynctrigger.js","default":"./lib/async/asynctrigger.js"},"./async/deferredpromise.js":{"node":"./lib/async/deferredpromise.js","browser":"./web/async/deferredpromise.js","react-native":"./web/async/deferredpromise.js","default":"./lib/async/deferredpromise.js"},"./async/eventqueue.js":{"node":"./lib/async/eventqueue.js","browser":"./web/async/eventqueue.js","react-native":"./web/async/eventqueue.js","default":"./lib/async/eventqueue.js"},"./async/keycache.js":{"node":"./lib/async/keycache.js","browser":"./web/async/keycache.js","react-native":"./web/async/keycache.js","default":"./lib/async/keycache.js"},"./async/queues.js":{"node":"./lib/async/queues.js","browser":"./web/async/queues.js","react-native":"./web/async/queues.js","default":"./lib/async/queues.js"},"./async/timecache.js":{"node":"./lib/async/timecache.js","browser":"./web/async/timecache.js","react-native":"./web/async/timecache.js","default":"./lib/async/timecache.js"},"./base58.js":{"node":"./lib/base58.js","browser":"./web/base58.js","react-native":"./web/base58.js","default":"./lib/base58.js"},"./base64.js":{"node":"./lib/base64.js","browser":"./web/base64.js","react-native":"./web/base64.js","default":"./lib/base64.js"},"./benchmark.js":{"node":"./lib/benchmark.js","browser":"./web/benchmark.js","react-native":"./web/benchmark.js","default":"./lib/benchmark.js"},"./bytebuffer.js":{"node":"./lib/bytebuffer.js","browser":"./web/bytebuffer.js","react-native":"./web/bytebuffer.js","default":"./lib/bytebuffer.js"},"./consts.js":{"node":"./lib/consts.js","browser":"./web/consts.js","react-native":"./web/consts.js","default":"./lib/consts.js"},"./cron.js":{"node":"./lib/cron.js","browser":"./web/cron.js","react-native":"./web/cron.js","default":"./lib/cron.js"},"./dataview.js":{"node":"./lib/dataview.js","browser":"./web/dataview.js","react-native":"./web/dataview.js","default":"./lib/dataview.js"},"./dict.js":{"node":"./lib/dict.js","browser":"./web/dict.js","react-native":"./web/dict.js","default":"./lib/dict.js"},"./error.js":{"node":"./lib/error.js","browser":"./web/error.js","react-native":"./web/error.js","default":"./lib/error.js"},"./global.js":{"node":"./lib/global.js","browser":"./web/global.js","react-native":"./web/global.js","default":"./lib/global.js"},"./hex.js":{"node":"./lib/hex.js","browser":"./web/hex.js","react-native":"./web/hex.js","default":"./lib/hex.js"},"./idx.js":{"node":"./lib/idx.js","browser":"./web/idx.js","react-native":"./web/idx.js","default":"./lib/idx.js"},"./json.js":{"node":"./lib/json.js","browser":"./web/json.js","react-native":"./web/json.js","default":"./lib/json.js"},"./linebuffer.js":{"node":"./lib/linebuffer.js","browser":"./web/linebuffer.js","react-native":"./web/linebuffer.js","default":"./lib/linebuffer.js"},"./marshalling/marshaller.js":{"node":"./lib/marshalling/marshaller.js","browser":"./web/marshalling/marshaller.js","react-native":"./web/marshalling/marshaller.js","default":"./lib/marshalling/marshaller.js"},"./marshalling/unmarshaller.js":{"node":"./lib/marshalling/unmarshaller.js","browser":"./web/marshalling/unmarshaller.js","react-native":"./web/marshalling/unmarshaller.js","default":"./lib/marshalling/unmarshaller.js"},"./number.js":{"node":"./lib/number.js","browser":"./web/number.js","react-native":"./web/number.js","default":"./lib/number.js"},"./path.js":{"node":"./lib/path.js","browser":"./web/path.js","react-native":"./web/path.js","default":"./lib/path.js"},"./promise.js":{"node":"./lib/promise.js","browser":"./web/promise.js","react-native":"./web/promise.js","default":"./lib/promise.js"},"./starttime.js":{"node":"./lib/starttime.js","browser":"./web/starttime.js","react-native":"./web/starttime.js","default":"./lib/starttime.js"},"./string.js":{"node":"./lib/string.js","browser":"./web/string.js","react-native":"./web/string.js","default":"./lib/string.js"},"./triggers.js":{"node":"./lib/triggers.js","browser":"./web/triggers.js","react-native":"./web/triggers.js","default":"./lib/triggers.js"},"./types/array.js":{"node":"./lib/types/array.js","browser":"./web/types/array.js","react-native":"./web/types/array.js","default":"./lib/types/array.js"},"./types/enum.js":{"node":"./lib/types/enum.js","browser":"./web/types/enum.js","react-native":"./web/types/enum.js","default":"./lib/types/enum.js"},"./types/predicateerror.js":{"node":"./lib/types/predicateerror.js","browser":"./web/types/predicateerror.js","react-native":"./web/types/predicateerror.js","default":"./lib/types/predicateerror.js"},"./types/primitive.js":{"node":"./lib/types/primitive.js","browser":"./web/types/primitive.js","react-native":"./web/types/primitive.js","default":"./lib/types/primitive.js"},"./types/record.js":{"node":"./lib/types/record.js","browser":"./web/types/record.js","react-native":"./web/types/record.js","default":"./lib/types/record.js"},"./types/types.js":{"node":"./lib/types/types.js","browser":"./web/types/types.js","react-native":"./web/types/types.js","default":"./lib/types/types.js"},"./types/utils.js":{"node":"./lib/types/utils.js","browser":"./web/types/utils.js","react-native":"./web/types/utils.js","default":"./lib/types/utils.js"},"./uint8array.js":{"node":"./lib/uint8array.js","browser":"./web/uint8array.js","react-native":"./web/uint8array.js","default":"./lib/uint8array.js"},"./units.js":{"node":"./lib/units.js","browser":"./web/units.js","react-native":"./web/units.js","default":"./lib/units.js"}},"files":["/lib","/web"]}
1
+ {"name":"@keeex/utils","version":"7.6.1","description":"Various utility functions for pure JavaScript","scripts":{},"author":"KeeeX SAS","contributors":[{"name":"Gabriel Paul \"Cley Faye\" Risterucci","email":"gabriel@keeex.net"}],"homepage":"https://keeex.me/oss","keywords":["utility"],"type":"module","license":"MIT","dependencies":{"@keeex/bubble_babble":"^3.0.1","@keeex/log":"^1.7.2","base64-arraybuffer":"^1.0.2","cron-parser":"^5.5.0","ms":"^2.1.3","text-encoding-shim":"^1.0.5"},"exports":{"./array.js":{"node":"./lib/array.js","browser":"./web/array.js","react-native":"./web/array.js","default":"./lib/array.js"},"./arraybuffer.js":{"node":"./lib/arraybuffer.js","browser":"./web/arraybuffer.js","react-native":"./web/arraybuffer.js","default":"./lib/arraybuffer.js"},"./async/asynctrigger.js":{"node":"./lib/async/asynctrigger.js","browser":"./web/async/asynctrigger.js","react-native":"./web/async/asynctrigger.js","default":"./lib/async/asynctrigger.js"},"./async/deferredpromise.js":{"node":"./lib/async/deferredpromise.js","browser":"./web/async/deferredpromise.js","react-native":"./web/async/deferredpromise.js","default":"./lib/async/deferredpromise.js"},"./async/eventqueue.js":{"node":"./lib/async/eventqueue.js","browser":"./web/async/eventqueue.js","react-native":"./web/async/eventqueue.js","default":"./lib/async/eventqueue.js"},"./async/keycache.js":{"node":"./lib/async/keycache.js","browser":"./web/async/keycache.js","react-native":"./web/async/keycache.js","default":"./lib/async/keycache.js"},"./async/queues.js":{"node":"./lib/async/queues.js","browser":"./web/async/queues.js","react-native":"./web/async/queues.js","default":"./lib/async/queues.js"},"./async/timecache.js":{"node":"./lib/async/timecache.js","browser":"./web/async/timecache.js","react-native":"./web/async/timecache.js","default":"./lib/async/timecache.js"},"./base58.js":{"node":"./lib/base58.js","browser":"./web/base58.js","react-native":"./web/base58.js","default":"./lib/base58.js"},"./base64.js":{"node":"./lib/base64.js","browser":"./web/base64.js","react-native":"./web/base64.js","default":"./lib/base64.js"},"./benchmark.js":{"node":"./lib/benchmark.js","browser":"./web/benchmark.js","react-native":"./web/benchmark.js","default":"./lib/benchmark.js"},"./bytebuffer.js":{"node":"./lib/bytebuffer.js","browser":"./web/bytebuffer.js","react-native":"./web/bytebuffer.js","default":"./lib/bytebuffer.js"},"./consts.js":{"node":"./lib/consts.js","browser":"./web/consts.js","react-native":"./web/consts.js","default":"./lib/consts.js"},"./cron.js":{"node":"./lib/cron.js","browser":"./web/cron.js","react-native":"./web/cron.js","default":"./lib/cron.js"},"./dataview.js":{"node":"./lib/dataview.js","browser":"./web/dataview.js","react-native":"./web/dataview.js","default":"./lib/dataview.js"},"./dict.js":{"node":"./lib/dict.js","browser":"./web/dict.js","react-native":"./web/dict.js","default":"./lib/dict.js"},"./error.js":{"node":"./lib/error.js","browser":"./web/error.js","react-native":"./web/error.js","default":"./lib/error.js"},"./global.js":{"node":"./lib/global.js","browser":"./web/global.js","react-native":"./web/global.js","default":"./lib/global.js"},"./hex.js":{"node":"./lib/hex.js","browser":"./web/hex.js","react-native":"./web/hex.js","default":"./lib/hex.js"},"./idx.js":{"node":"./lib/idx.js","browser":"./web/idx.js","react-native":"./web/idx.js","default":"./lib/idx.js"},"./json.js":{"node":"./lib/json.js","browser":"./web/json.js","react-native":"./web/json.js","default":"./lib/json.js"},"./linebuffer.js":{"node":"./lib/linebuffer.js","browser":"./web/linebuffer.js","react-native":"./web/linebuffer.js","default":"./lib/linebuffer.js"},"./marshalling/marshaller.js":{"node":"./lib/marshalling/marshaller.js","browser":"./web/marshalling/marshaller.js","react-native":"./web/marshalling/marshaller.js","default":"./lib/marshalling/marshaller.js"},"./marshalling/unmarshaller.js":{"node":"./lib/marshalling/unmarshaller.js","browser":"./web/marshalling/unmarshaller.js","react-native":"./web/marshalling/unmarshaller.js","default":"./lib/marshalling/unmarshaller.js"},"./number.js":{"node":"./lib/number.js","browser":"./web/number.js","react-native":"./web/number.js","default":"./lib/number.js"},"./path.js":{"node":"./lib/path.js","browser":"./web/path.js","react-native":"./web/path.js","default":"./lib/path.js"},"./promise.js":{"node":"./lib/promise.js","browser":"./web/promise.js","react-native":"./web/promise.js","default":"./lib/promise.js"},"./starttime.js":{"node":"./lib/starttime.js","browser":"./web/starttime.js","react-native":"./web/starttime.js","default":"./lib/starttime.js"},"./string.js":{"node":"./lib/string.js","browser":"./web/string.js","react-native":"./web/string.js","default":"./lib/string.js"},"./triggers.js":{"node":"./lib/triggers.js","browser":"./web/triggers.js","react-native":"./web/triggers.js","default":"./lib/triggers.js"},"./types/array.js":{"node":"./lib/types/array.js","browser":"./web/types/array.js","react-native":"./web/types/array.js","default":"./lib/types/array.js"},"./types/enum.js":{"node":"./lib/types/enum.js","browser":"./web/types/enum.js","react-native":"./web/types/enum.js","default":"./lib/types/enum.js"},"./types/predicateerror.js":{"node":"./lib/types/predicateerror.js","browser":"./web/types/predicateerror.js","react-native":"./web/types/predicateerror.js","default":"./lib/types/predicateerror.js"},"./types/primitive.js":{"node":"./lib/types/primitive.js","browser":"./web/types/primitive.js","react-native":"./web/types/primitive.js","default":"./lib/types/primitive.js"},"./types/record.js":{"node":"./lib/types/record.js","browser":"./web/types/record.js","react-native":"./web/types/record.js","default":"./lib/types/record.js"},"./types/types.js":{"node":"./lib/types/types.js","browser":"./web/types/types.js","react-native":"./web/types/types.js","default":"./lib/types/types.js"},"./types/utils.js":{"node":"./lib/types/utils.js","browser":"./web/types/utils.js","react-native":"./web/types/utils.js","default":"./lib/types/utils.js"},"./uint8array.js":{"node":"./lib/uint8array.js","browser":"./web/uint8array.js","react-native":"./web/uint8array.js","default":"./lib/uint8array.js"},"./units.js":{"node":"./lib/units.js","browser":"./web/units.js","react-native":"./web/units.js","default":"./lib/units.js"}},"files":["/lib","/web"]}
package/web/array.d.ts CHANGED
@@ -13,6 +13,7 @@
13
13
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
14
14
  *
15
15
  */
16
+ import type { Arrayable } from "./types/array.js";
16
17
  export type EqualityFunction<T1, T2> = (op1: T1, op2: T2) => boolean;
17
18
  /**
18
19
  * Compare two array-like structure for equal content
@@ -25,4 +26,6 @@ export declare const arrayEqual: <T1 = unknown, T2 = unknown>(op1: ArrayLike<T1>
25
26
  *
26
27
  * @public
27
28
  */
28
- export declare const asArray: <T>(data: Array<T> | (T extends Array<unknown> ? never : T)) => Array<T>;
29
+ export declare const asArray: <T>(data: Arrayable<T>) => Array<T>;
30
+ /** Return a new array with its string sorted */
31
+ export declare const toSortedStringArray: (array: Array<string>) => Array<string>;
package/web/array.js CHANGED
@@ -13,6 +13,7 @@
13
13
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
14
14
  *
15
15
  */
16
+ import { alphaSort } from "./string.js";
16
17
  const useEqOp = (op1, op2) => op1 === op2;
17
18
  /**
18
19
  * Compare two array-like structure for equal content
@@ -31,4 +32,6 @@ export const arrayEqual = (op1, op2, eqFunc = useEqOp) => {
31
32
  *
32
33
  * @public
33
34
  */
34
- export const asArray = data => Array.isArray(data) ? data : [data];
35
+ export const asArray = data => Array.isArray(data) ? data : [data];
36
+ /** Return a new array with its string sorted */
37
+ export const toSortedStringArray = array => array.toSorted(alphaSort);
@@ -17,6 +17,8 @@
17
17
  * Allows returning a promise and resolving it in another scope
18
18
  *
19
19
  * @public
20
+ *
21
+ * @deprecated Use `Promise.withResolvers()`
20
22
  */
21
23
  export declare class DeferredPromise<ResultType> {
22
24
  #private;
@@ -19,6 +19,8 @@
19
19
  * Allows returning a promise and resolving it in another scope
20
20
  *
21
21
  * @public
22
+ *
23
+ * @deprecated Use `Promise.withResolvers()`
22
24
  */
23
25
  export class DeferredPromise {
24
26
  /** The actual promise that can be awaited */
package/web/bits/hex.d.ts CHANGED
@@ -13,5 +13,12 @@
13
13
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
14
14
  *
15
15
  */
16
- /** Clean an hex string input; make it even and remove 0x prefix if present */
16
+ /** Remove the 0x prefix if present. */
17
+ export declare const getHexNoPrefix: (input: string) => string;
18
+ /** Ensure that the input is a valid hexadecimal string (only allow `[0-9a-fA-F]`) */
19
+ export declare const isHexString: (input: string) => boolean;
20
+ /**
21
+ * Clean an hex string input; make it even and remove 0x prefix if present.
22
+ * Also convert it to lowercase and check if the input is actually an hexadecimal string.
23
+ */
17
24
  export declare const clearHexInput: (input: string) => string;
package/web/bits/hex.js CHANGED
@@ -16,12 +16,38 @@
16
16
  const CHARACTER_PER_BYTE = 2;
17
17
  const HEX_PREFIX = "0x";
18
18
  const HEX_PREFIX_LENGTH = HEX_PREFIX.length;
19
- /** Clean an hex string input; make it even and remove 0x prefix if present */
19
+ /** Remove the 0x prefix if present. */
20
+ export const getHexNoPrefix = input => input.startsWith(HEX_PREFIX) ? input.substring(HEX_PREFIX_LENGTH) : input;
21
+ const HEX_RANGES_NUMBERS = {
22
+ min: "0".charCodeAt(0),
23
+ max: "9".charCodeAt(0)
24
+ };
25
+ const HEX_RANGES_LOWERCASE = {
26
+ min: "a".charCodeAt(0),
27
+ max: "f".charCodeAt(0)
28
+ };
29
+ const HEX_RANGES_UPPERCASE = {
30
+ min: "A".charCodeAt(0),
31
+ max: "F".charCodeAt(0)
32
+ };
33
+ /** Ensure that the input is a valid hexadecimal string (only allow `[0-9a-fA-F]`) */
34
+ export const isHexString = input => {
35
+ for (let i = 0; i < input.length; ++i) {
36
+ const charCode = input.charCodeAt(i);
37
+ if ((charCode < HEX_RANGES_NUMBERS.min || charCode > HEX_RANGES_NUMBERS.max) && (charCode < HEX_RANGES_LOWERCASE.min || charCode > HEX_RANGES_LOWERCASE.max) && (charCode < HEX_RANGES_UPPERCASE.min || charCode > HEX_RANGES_UPPERCASE.max)) {
38
+ return false;
39
+ }
40
+ }
41
+ return true;
42
+ };
43
+ /**
44
+ * Clean an hex string input; make it even and remove 0x prefix if present.
45
+ * Also convert it to lowercase and check if the input is actually an hexadecimal string.
46
+ */
20
47
  export const clearHexInput = input => {
21
- const noprefix = !input.startsWith(HEX_PREFIX);
48
+ const withoutPrefix = getHexNoPrefix(input);
49
+ if (!isHexString(withoutPrefix)) throw new Error("Input is not a valid hexadecimal string");
22
50
  const even = input.length % CHARACTER_PER_BYTE === 0;
23
- if (noprefix && even) return input;
24
- if (noprefix) return `0${input}`;
25
- if (even) return input.substring(HEX_PREFIX_LENGTH);
26
- return `0${input.substring(HEX_PREFIX_LENGTH)}`;
51
+ if (even) return withoutPrefix.toLowerCase();
52
+ return `0${withoutPrefix.toLowerCase()}`;
27
53
  };
package/web/dict.d.ts CHANGED
@@ -54,3 +54,35 @@ export type PrimitiveTypeObject = Record<string, unknown> | Array<unknown> | str
54
54
  * @public
55
55
  */
56
56
  export declare const deepCopyPrimitive: (source: unknown) => unknown;
57
+ /**
58
+ * Perform the merge operation between two values.
59
+ *
60
+ * If the new value have no changes from the initial value, this should always return `initial`.
61
+ * If there is any change, this should always return a new reference.
62
+ *
63
+ * TODO: The function returns unknown because I can't work around the requirements for TypeScript to
64
+ * be happy using `DataType | null`.
65
+ */
66
+ type RefMergeDefFn<DataType> = (initial: DataType | null, newValue: DataType | null) => unknown;
67
+ type RefMergeDef<DataType> = DataType extends string ? "string" : DataType extends number ? "number" : DataType extends boolean ? "boolean" : DataType extends Array<string> ? "string[]" : RefMergeDefFn<DataType>;
68
+ type RefMergeProfile<ObjectType> = Required<{
69
+ [key in keyof ObjectType]: RefMergeDef<NonNullable<ObjectType[key]>>;
70
+ }>;
71
+ /**
72
+ * Examine each props on both `initial` and `newValue`, and if there is any change, return a new
73
+ * reference with all values.
74
+ *
75
+ * @param initial - The source object. If no changes are detected, returns it.
76
+ * @param newValue - Values to update in `initial`. `undefined` values are skipped.
77
+ * @param profile - Describe the expected properties and how to merge them.
78
+ *
79
+ * @returns
80
+ * If there is some change from `initial`, will return a new reference with all new values.
81
+ * This property extends to values that are object; all objects in this hierarchy that have an
82
+ * update belows it is replaced with a new reference.
83
+ *
84
+ * This is made to help with libraries like React that depends on state reference actually getting
85
+ * changed on update.
86
+ */
87
+ export declare const refMergeObjects: <ObjectType>(initial: ObjectType, newValue: Partial<ObjectType>, profile: RefMergeProfile<ObjectType>) => ObjectType;
88
+ export {};
package/web/dict.js CHANGED
@@ -13,6 +13,7 @@
13
13
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
14
14
  *
15
15
  */
16
+ import { arrayEqual } from "./array.js";
16
17
  /**
17
18
  * Copy all properties from secondary into primary.
18
19
  *
@@ -64,4 +65,69 @@ export const deepCopyPrimitive = source => {
64
65
  }
65
66
  return res;
66
67
  }
68
+ };
69
+ const refMergeStringArray = (initial, newValue) => {
70
+ if (newValue === null) return null;
71
+ if (initial !== null && arrayEqual(initial, newValue)) return initial;
72
+ return [...newValue];
73
+ };
74
+ /**
75
+ * Merge a new value and an old value according to the provided merger.
76
+ *
77
+ * If `newValue` is undefined, `initial` is always returned.
78
+ * If `newValue` is defined AND have difference from `initial`, a new object should be returned.
79
+ * If there is no change, `initial` is returned.
80
+ */
81
+ const getMergedRef = (initial, newValue, merger) => {
82
+ if (newValue === undefined) return initial;
83
+ switch (merger) {
84
+ case "string":
85
+ case "number":
86
+ case "boolean":
87
+ return newValue;
88
+ case "string[]":
89
+ return refMergeStringArray(initial, newValue);
90
+ default:
91
+ {
92
+ return merger(initial, newValue);
93
+ }
94
+ }
95
+ };
96
+ /**
97
+ * Examine each props on both `initial` and `newValue`, and if there is any change, return a new
98
+ * reference with all values.
99
+ *
100
+ * @param initial - The source object. If no changes are detected, returns it.
101
+ * @param newValue - Values to update in `initial`. `undefined` values are skipped.
102
+ * @param profile - Describe the expected properties and how to merge them.
103
+ *
104
+ * @returns
105
+ * If there is some change from `initial`, will return a new reference with all new values.
106
+ * This property extends to values that are object; all objects in this hierarchy that have an
107
+ * update belows it is replaced with a new reference.
108
+ *
109
+ * This is made to help with libraries like React that depends on state reference actually getting
110
+ * changed on update.
111
+ */
112
+ export const refMergeObjects = (initial, newValue, profile) => {
113
+ let anyChange = false;
114
+ const newValues = Object.entries(profile).map(([key, merger]) => {
115
+ const initialValue = initial[key];
116
+ const merged = getMergedRef(initialValue, newValue[key], merger);
117
+ anyChange ||= merged !== initialValue;
118
+ return {
119
+ key,
120
+ merged
121
+ };
122
+ });
123
+ // Can't statically detect changes in map() callback
124
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
125
+ if (!anyChange) return initial;
126
+ return newValues.reduce((acc, {
127
+ key,
128
+ merged
129
+ }) => {
130
+ acc[key] = merged;
131
+ return acc;
132
+ }, {});
67
133
  };
package/web/string.d.ts CHANGED
@@ -63,3 +63,11 @@ export declare const encodeUTF8: (str: string) => string;
63
63
  export declare const decodeUTF8: (str: string, skipErrors?: boolean) => string;
64
64
  /** Capitalize (or uncapitalize) the first character of a string */
65
65
  export declare const capitalize: (str: string, upperFirstLetter: boolean) => string;
66
+ /** Ensure the string ends with one newline */
67
+ export declare const enforceNewlineEnd: (str: string) => string;
68
+ /** Format the text as a single line or multiline JS comment */
69
+ export declare const jsComment: (text: string, jsDoc?: boolean) => string;
70
+ /** Make sure the string starts with a capital, and all other letters are lowercase */
71
+ export declare const uncapitalize: (str: string) => string;
72
+ /** Helper to use with `Array.sort()` and similar */
73
+ export declare const alphaSort: (a: string, b: string) => number;
package/web/string.js CHANGED
@@ -98,4 +98,32 @@ export const capitalize = (str, upperFirstLetter) => {
98
98
  const transformedFirstLetter = upperFirstLetter ? firstLetter.toUpperCase() : firstLetter.toLowerCase();
99
99
  const rest = str.slice(1);
100
100
  return `${transformedFirstLetter}${rest}`;
101
- };
101
+ };
102
+ /** Return the last index of a character that pass the needle test, or -1 if it never happens */
103
+ const findLastIndexOf = (hay, needle) => {
104
+ for (let index = hay.length - 1; --index; index >= 0) {
105
+ if (needle(hay.at(index))) return index;
106
+ }
107
+ return -1;
108
+ };
109
+ /** Ensure the string ends with one newline */
110
+ export const enforceNewlineEnd = str => {
111
+ if (str.endsWith("\n")) {
112
+ const firstNonNewline = findLastIndexOf(str, c => c !== "\n");
113
+ if (firstNonNewline === -1) return "\n";
114
+ return `${str.slice(0, firstNonNewline + 1)}\n`;
115
+ }
116
+ return `${str}\n`;
117
+ };
118
+ /** Format the text as a single line or multiline JS comment */
119
+ export const jsComment = (text, jsDoc = false) => {
120
+ const lines = text.split("\n");
121
+ if (lines.length === 1 && !jsDoc) return `// ${lines[0]}`;
122
+ const prefix = jsDoc ? "/**" : "/*";
123
+ if (lines.length === 1) return `${prefix} ${lines[0].replaceAll("*/", "* /")} */`;
124
+ return [prefix, ...lines.map(c => ` * ${c.trimEnd().replaceAll("*/", "* /")}`), " */"].join("\n");
125
+ };
126
+ /** Make sure the string starts with a capital, and all other letters are lowercase */
127
+ export const uncapitalize = str => str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
128
+ /** Helper to use with `Array.sort()` and similar */
129
+ export const alphaSort = (a, b) => a.localeCompare(b);