@develia/commons 0.4.25 → 0.5.0

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.
Files changed (81) hide show
  1. package/dist/array.d.ts +10 -0
  2. package/dist/array.d.ts.map +1 -1
  3. package/dist/array.js +18 -0
  4. package/dist/array.js.map +1 -1
  5. package/dist/async-pool-executor.d.ts +3 -5
  6. package/dist/async-pool-executor.d.ts.map +1 -1
  7. package/dist/async-pool-executor.js +9 -9
  8. package/dist/async-pool-executor.js.map +1 -1
  9. package/dist/browser/form-utilities.d.ts +0 -2
  10. package/dist/browser/form-utilities.d.ts.map +1 -1
  11. package/dist/browser/form-utilities.js +3 -23
  12. package/dist/browser/form-utilities.js.map +1 -1
  13. package/dist/browser/geo-location.js +3 -3
  14. package/dist/browser/geo-location.js.map +1 -1
  15. package/dist/browser/native-bridge.d.ts +1 -1
  16. package/dist/browser/native-bridge.d.ts.map +1 -1
  17. package/dist/browser/native-bridge.js +2 -2
  18. package/dist/browser/native-bridge.js.map +1 -1
  19. package/dist/cancellable-promise.d.ts +8 -2
  20. package/dist/cancellable-promise.d.ts.map +1 -1
  21. package/dist/cancellable-promise.js +9 -3
  22. package/dist/cancellable-promise.js.map +1 -1
  23. package/dist/clean-url-search-params.js +3 -3
  24. package/dist/clean-url-search-params.js.map +1 -1
  25. package/dist/collections/from.d.ts +31 -11
  26. package/dist/collections/from.d.ts.map +1 -1
  27. package/dist/collections/from.js +73 -48
  28. package/dist/collections/from.js.map +1 -1
  29. package/dist/draft.d.ts +2 -2
  30. package/dist/draft.d.ts.map +1 -1
  31. package/dist/draft.js +2 -2
  32. package/dist/draft.js.map +1 -1
  33. package/dist/functions.d.ts +2 -2
  34. package/dist/functions.d.ts.map +1 -1
  35. package/dist/functions.js +1 -1
  36. package/dist/functions.js.map +1 -1
  37. package/dist/index.d.ts +2 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +2 -1
  40. package/dist/index.js.map +1 -1
  41. package/dist/math.d.ts +14 -5
  42. package/dist/math.d.ts.map +1 -1
  43. package/dist/math.js +16 -5
  44. package/dist/math.js.map +1 -1
  45. package/dist/string.d.ts +5 -0
  46. package/dist/string.d.ts.map +1 -1
  47. package/dist/string.js +8 -3
  48. package/dist/string.js.map +1 -1
  49. package/dist/timespan.d.ts +5 -5
  50. package/dist/timespan.d.ts.map +1 -1
  51. package/dist/timespan.js +5 -5
  52. package/dist/timespan.js.map +1 -1
  53. package/dist/types.d.ts +0 -1
  54. package/dist/types.d.ts.map +1 -1
  55. package/dist/types.js.map +1 -1
  56. package/dist/utilities.d.ts +11 -9
  57. package/dist/utilities.d.ts.map +1 -1
  58. package/dist/utilities.js +23 -23
  59. package/dist/utilities.js.map +1 -1
  60. package/dist/uuid.d.ts +73 -0
  61. package/dist/uuid.d.ts.map +1 -0
  62. package/dist/uuid.js +129 -0
  63. package/dist/uuid.js.map +1 -0
  64. package/package.json +5 -1
  65. package/src/array.ts +19 -0
  66. package/src/async-pool-executor.ts +9 -9
  67. package/src/browser/form-utilities.ts +3 -32
  68. package/src/browser/geo-location.ts +3 -3
  69. package/src/browser/native-bridge.ts +2 -2
  70. package/src/cancellable-promise.ts +9 -3
  71. package/src/clean-url-search-params.ts +3 -3
  72. package/src/collections/from.ts +91 -49
  73. package/src/draft.ts +3 -3
  74. package/src/functions.ts +3 -3
  75. package/src/index.ts +2 -1
  76. package/src/math.ts +17 -5
  77. package/src/string.ts +9 -3
  78. package/src/timespan.ts +5 -5
  79. package/src/types.ts +0 -1
  80. package/src/utilities.ts +27 -29
  81. package/src/uuid.ts +153 -0
package/src/draft.ts CHANGED
@@ -39,7 +39,7 @@ type DraftMethods<T> = {
39
39
  *
40
40
  * @returns Array of modified property keys (does not recurse into children).
41
41
  */
42
- modifiedProps(): (keyof T)[];
42
+ getModifiedProps(): (keyof T)[];
43
43
 
44
44
  /**
45
45
  * Manually mark (or unmark) a property as modified.
@@ -155,7 +155,7 @@ function deepDraft(value: any, parentLink?: ParentLink): any {
155
155
  /**
156
156
  * Create a draft proxy over `original`. The proxy exposes the same properties
157
157
  * as the original (with nested objects wrapped as child drafts) plus the
158
- * control methods `commit`, `rollback`, `isModified`, `modifiedProps` and
158
+ * control methods `commit`, `rollback`, `isModified`, `getModifiedProps` and
159
159
  * `setModified`.
160
160
  *
161
161
  * Mutations on the draft do not affect the original until `commit()` is called.
@@ -249,7 +249,7 @@ export default function draft<T extends object>(
249
249
  return key === undefined ? touched.size > 0 : touched.has(key);
250
250
  },
251
251
 
252
- modifiedProps() {
252
+ getModifiedProps() {
253
253
  return [...touched] as (keyof T)[];
254
254
  },
255
255
 
package/src/functions.ts CHANGED
@@ -17,7 +17,7 @@ export interface ThrottleOptions {
17
17
  * const refresh = throttle(loadData, 250);
18
18
  * refresh();
19
19
  *
20
- * if (refresh.pending()) {
20
+ * if (refresh.isPending()) {
21
21
  * refresh.flush();
22
22
  * }
23
23
  * ```
@@ -31,7 +31,7 @@ export type DeferredFunction<F extends AnyFunction> = ((
31
31
  /** Execute the scheduled trailing call immediately, when one exists. */
32
32
  flush(): ReturnType<F> | undefined;
33
33
  /** Return true while a trailing call is scheduled. */
34
- pending(): boolean;
34
+ isPending(): boolean;
35
35
  };
36
36
 
37
37
  /**
@@ -300,7 +300,7 @@ export function throttle<F extends AnyFunction>(
300
300
  return result;
301
301
  };
302
302
 
303
- throttled.pending = (): boolean => timer !== undefined;
303
+ throttled.isPending = (): boolean => timer !== undefined;
304
304
 
305
305
  return throttled;
306
306
  }
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  export * from './types.js';
5
5
  export {default as Type} from './type.js';
6
6
  export {default as TypeCode} from './type-code.js';
7
+ export {default as Uuid} from './uuid.js';
7
8
 
8
9
  // ─── Utilities ───────────────────────────────────────
9
10
  export * from './utilities.js';
@@ -41,7 +42,7 @@ export {default as draft} from './draft.js';
41
42
  // ─── Web ─────────────────────────────────────────────
42
43
  export {default as CleanURLSearchParams} from './clean-url-search-params.js';
43
44
  /** @deprecated */
44
- export {ajaxSubmit, ajaxSubmission, submitForm, formToObject} from './browser/index.js';
45
+ export {ajaxSubmit, submitForm, formToObject} from './browser/index.js';
45
46
 
46
47
 
47
48
 
package/src/math.ts CHANGED
@@ -1,15 +1,27 @@
1
1
  /**
2
- * Linearly interpolates a number between two ranges.
2
+ * Linearly interpolates between two numbers: `a` for `t` 0, `b` for `t` 1, and beyond them outside that range.
3
3
  *
4
- * @param {number} n - The number to interpolate.
4
+ * @param {number} a - The value at `t` 0.
5
+ * @param {number} b - The value at `t` 1.
6
+ * @param {number} t - The interpolation factor.
7
+ * @return {number} - The interpolated value.
8
+ */
9
+ export function lerp(a: number, b: number, t: number): number {
10
+ return a + (b - a) * t;
11
+ }
12
+
13
+ /**
14
+ * Maps a number from one range to another, keeping its proportion (`remap(5, 0, 10, 0, 100)` is 50).
15
+ *
16
+ * @param {number} n - The number to map.
5
17
  * @param {number} inMin - The minimum value of the input range.
6
18
  * @param {number} inMax - The maximum value of the input range.
7
19
  * @param {number} outMin - The minimum value of the output range.
8
20
  * @param {number} outMax - The maximum value of the output range.
9
- *
10
- * @return {number} - The interpolated value.
21
+ * @return {number} - The number in the output range.
22
+ * @throws {Error} When the input range is empty (`inMin === inMax`).
11
23
  */
12
- export function lerp(n: number, inMin: number, inMax: number, outMin: number, outMax: number): number {
24
+ export function remap(n: number, inMin: number, inMax: number, outMin: number, outMax: number): number {
13
25
  if (inMax === inMin) {
14
26
  throw new Error("Input range cannot be zero");
15
27
  }
package/src/string.ts CHANGED
@@ -26,14 +26,20 @@ export function ensureSuffix(str: string, suffix: string): string {
26
26
  return str
27
27
  }
28
28
 
29
+ /**
30
+ * Null for an empty string, as `Str::nullIfEmpty()` of develia/commons for PHP; any other string as it is. Compose it
31
+ * with `trim()` to turn a blank string into null: `nullIfEmpty(value.trim())`.
32
+ */
33
+ export function nullIfEmpty(str: string | null | undefined): string | null {
34
+ return str === undefined || str === null || str === '' ? null : str;
35
+ }
36
+
29
37
  /**
30
38
  * Normalizes Unicode whitespaces into single ASCII spaces and removes leading
31
39
  * and trailing whitespace.
32
40
  */
33
41
  export function normalizeWhitespaces(str: string): string {
34
- return str.normalize('NFKC')
35
- .replace(/[\s\u200B\uFEFF]+/gu, ' ')
36
- .trim()
42
+ return str.replace(/[\s\u200B\uFEFF]+/gu, ' ').trim();
37
43
  }
38
44
 
39
45
  /**
package/src/timespan.ts CHANGED
@@ -13,7 +13,7 @@ export default class TimeSpan {
13
13
  * Gets the time interval in seconds.
14
14
  * @returns {number} The duration in seconds.
15
15
  */
16
- seconds(): number {
16
+ getTotalSeconds(): number {
17
17
  return this.milliseconds / 1000;
18
18
  }
19
19
 
@@ -21,7 +21,7 @@ export default class TimeSpan {
21
21
  * Gets the time interval in minutes.
22
22
  * @returns {number} The duration in minutes.
23
23
  */
24
- minutes(): number {
24
+ getTotalMinutes(): number {
25
25
  return this.milliseconds / (1000 * 60);
26
26
  }
27
27
 
@@ -29,7 +29,7 @@ export default class TimeSpan {
29
29
  * Gets the time interval in hours.
30
30
  * @returns {number} The duration in hours.
31
31
  */
32
- hours(): number {
32
+ getTotalHours(): number {
33
33
  return this.milliseconds / (1000 * 60 * 60);
34
34
  }
35
35
 
@@ -37,7 +37,7 @@ export default class TimeSpan {
37
37
  * Gets the time interval in days.
38
38
  * @returns {number} The duration in days.
39
39
  */
40
- days(): number {
40
+ getTotalDays(): number {
41
41
  return this.milliseconds / (1000 * 60 * 60 * 24);
42
42
  }
43
43
 
@@ -45,7 +45,7 @@ export default class TimeSpan {
45
45
  * Gets the time interval in weeks.
46
46
  * @returns {number} The duration in weeks.
47
47
  */
48
- weeks(): number {
48
+ getTotalWeeks(): number {
49
49
  return this.milliseconds / (1000 * 60 * 60 * 24 * 7);
50
50
  }
51
51
 
package/src/types.ts CHANGED
@@ -12,7 +12,6 @@ export type Nullable<T> = T | null;
12
12
 
13
13
  export type Nullish = null | undefined;
14
14
 
15
- export type Resolvable<T> = T | Promise<T> | Supplier<T>;
16
15
 
17
16
  export type UnaryFunction<T, R> = (arg: T) => R;
18
17
 
package/src/utilities.ts CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
 
4
4
  import KeyValuePair from "./key-value-pair.js";
5
- import type {Resolvable, Constructor} from "./types.js";
5
+ import type {Constructor} from "./types.js";
6
6
  import Type from "./type.js";
7
7
 
8
8
  export function lazy<T extends object>(factory: () => T): T {
@@ -185,8 +185,8 @@ function _deepEqual(
185
185
  }
186
186
 
187
187
  const objB = b as object;
188
- const keysA = _deepEqualKeys(objA, includeInherited);
189
- const keysB = _deepEqualKeys(objB, includeInherited);
188
+ const keysA = _collectComparableKeys(objA, includeInherited);
189
+ const keysB = _collectComparableKeys(objB, includeInherited);
190
190
  if (keysA.length !== keysB.length) return false;
191
191
 
192
192
  for (const key of keysA) {
@@ -265,7 +265,7 @@ function _deepEqualSet(
265
265
  return true;
266
266
  }
267
267
 
268
- function _deepEqualKeys(value: object, includeInherited: boolean): PropertyKey[] {
268
+ function _collectComparableKeys(value: object, includeInherited: boolean): PropertyKey[] {
269
269
  const keys: PropertyKey[] = [];
270
270
  const seen = new Set<PropertyKey>();
271
271
  const addKey = (key: PropertyKey) => {
@@ -491,12 +491,13 @@ export function isEmptyOrWhitespace(value: any): boolean {
491
491
  /**
492
492
  * Converts an object into an array of key-value pairs.
493
493
  *
494
- * @param {Record<string, any>} obj - The object to convert.
495
- * @return {KeyValuePair<string, any>[]} - The array of key-value pairs.
494
+ * @template TValue - The type of the property values.
495
+ * @param {Record<string, TValue>} obj - The object to convert.
496
+ * @return {KeyValuePair<string, TValue>[]} - The array of key-value pairs.
496
497
  */
497
- export function toPairs(obj: Record<string, any>): KeyValuePair<string, any>[] {
498
+ export function toPairs<TValue>(obj: Record<string, TValue>): KeyValuePair<string, TValue>[] {
498
499
 
499
- let output = [];
500
+ const output: KeyValuePair<string, TValue>[] = [];
500
501
  for (const key in obj) {
501
502
  output.push(new KeyValuePair(key, obj[key]));
502
503
  }
@@ -528,29 +529,26 @@ export function pick<T extends object, K extends keyof T>(obj: T, props: K[]): P
528
529
  }
529
530
 
530
531
  /**
531
- * Converts a given thing into a Promise.
532
- * @template T
533
- * @param {any} thing - The thing to be converted into a Promise.
534
- * @returns {Promise<T>} - A Promise representing the given thing.
532
+ * Turns a function that takes a Node-style callback as its last argument (`(error, result) => void`) into one that
533
+ * returns a promise, as Node's `util.promisify`: it resolves with the result, or rejects with the error the callback
534
+ * receives. The function keeps the `this` it is called with.
535
+ * @param fn - The function to wrap.
536
+ * @returns The function that returns a promise.
535
537
  */
536
- export function promisify<T>(thing: Resolvable<T>): Promise<T> {
537
-
538
- if (thing instanceof Promise)
539
- return thing;
540
-
541
-
542
- if (isFunction(thing)) {
543
- return new Promise<T>((resolve, reject) => {
544
- try {
545
- const result = thing();
546
- resolve(result);
547
- } catch (error) {
548
- reject(error);
549
- }
538
+ export function promisify<TArgs extends unknown[], TResult>(
539
+ fn: (...args: [...TArgs, (error: unknown, result: TResult) => void]) => void,
540
+ ): (...args: TArgs) => Promise<TResult> {
541
+ return function (this: unknown, ...args: TArgs): Promise<TResult> {
542
+ return new Promise<TResult>((resolve, reject) => {
543
+ fn.call(this, ...args, (error: unknown, result: TResult) => {
544
+ if (error !== null && error !== undefined) {
545
+ reject(error);
546
+ } else {
547
+ resolve(result);
548
+ }
549
+ });
550
550
  });
551
- }
552
-
553
- return Promise.resolve(thing);
551
+ };
554
552
  }
555
553
 
556
554
 
package/src/uuid.ts ADDED
@@ -0,0 +1,153 @@
1
+ import {Nullable} from './types.js';
2
+
3
+ /** Versions 1 to 8 with the RFC variant, plus the Nil and Max UUIDs of RFC 9562 sections 5.9 and 5.10. */
4
+ const PATTERN = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|0{8}-0{4}-0{4}-0{4}-0{12}|f{8}-f{4}-f{4}-f{4}-f{12})$/i;
5
+
6
+ /** Largest Unix timestamp in milliseconds that fits in the 48 bits of a version 7 UUID. */
7
+ const MAX_V7_MILLISECONDS = 0xffffffffffff;
8
+
9
+ /**
10
+ * Immutable universally unique identifier (RFC 9562), kept in lowercase canonical form,
11
+ * such as `019a4f8b-4bae-7d2e-9f10-4a5b6c7d8e9f`.
12
+ *
13
+ * It serializes to its string with `JSON.stringify`, so DTOs carry it as text; read those
14
+ * strings back with {@link Uuid.parse} or {@link Uuid.parseOrThrow}.
15
+ */
16
+ export default class Uuid {
17
+ private readonly value: string;
18
+
19
+ private constructor(value: string) {
20
+ this.value = value;
21
+ }
22
+
23
+ /**
24
+ * Generates a version 4 UUID from 122 random bits of a cryptographically secure source.
25
+ *
26
+ * Unlike `crypto.randomUUID()`, it also works in browser contexts that are not secure, such as
27
+ * a development site served over HTTP.
28
+ */
29
+ static generateV4(): Uuid {
30
+ const bytes = new Uint8Array(16);
31
+ globalThis.crypto.getRandomValues(bytes);
32
+
33
+ return Uuid.createWithVersion(bytes, 4);
34
+ }
35
+
36
+ /**
37
+ * Generates a version 7 UUID: a 48-bit Unix timestamp in milliseconds followed by 74 random bits
38
+ * from a cryptographically secure source, so identifiers sort by creation time.
39
+ *
40
+ * Identifiers generated within the same millisecond keep no particular order among themselves.
41
+ *
42
+ * @param date Moment the UUID encodes; the current time when omitted.
43
+ * @throws {RangeError} If the date is invalid, before the Unix epoch or beyond the 48-bit range.
44
+ */
45
+ static generateV7(date: Date = new Date()): Uuid {
46
+ const milliseconds = date.getTime();
47
+ if (!Number.isInteger(milliseconds) || milliseconds < 0 || milliseconds > MAX_V7_MILLISECONDS) {
48
+ throw new RangeError('A version 7 UUID can only encode a valid date from the Unix epoch on.');
49
+ }
50
+
51
+ const bytes = new Uint8Array(16);
52
+ globalThis.crypto.getRandomValues(bytes);
53
+ let remaining = milliseconds;
54
+ for (let index = 5; index >= 0; index--) {
55
+ bytes[index] = remaining % 256;
56
+ remaining = Math.floor(remaining / 256);
57
+ }
58
+
59
+ return Uuid.createWithVersion(bytes, 7);
60
+ }
61
+
62
+ /**
63
+ * Checks if a value is a UUID in canonical form, in any letter case: one of RFC 9562 versions 1 to 8,
64
+ * or its Nil UUID (all zeros) or Max UUID (all ones).
65
+ *
66
+ * It returns a plain boolean rather than a type guard: a string that is not a UUID is still a
67
+ * string, which a guard would narrow to `never`.
68
+ */
69
+ static isValid(value: unknown): boolean {
70
+ return typeof value === 'string' && PATTERN.test(value);
71
+ }
72
+
73
+ /**
74
+ * Reads a UUID in canonical form, in any letter case, as {@link Uuid.isValid} accepts it.
75
+ *
76
+ * @returns The UUID, or `null` when the value is not one.
77
+ */
78
+ static parse(value: unknown): Nullable<Uuid> {
79
+ if (typeof value !== 'string' || !Uuid.isValid(value)) {
80
+ return null;
81
+ }
82
+
83
+ return new Uuid(value.toLowerCase());
84
+ }
85
+
86
+ /**
87
+ * Reads a UUID in canonical form, in any letter case.
88
+ *
89
+ * @throws {SyntaxError} If the value is not a UUID.
90
+ */
91
+ static parseOrThrow(value: string): Uuid {
92
+ const uuid = Uuid.parse(value);
93
+ if (uuid === null) {
94
+ throw new SyntaxError(`Invalid UUID "${value}".`);
95
+ }
96
+
97
+ return uuid;
98
+ }
99
+
100
+ /**
101
+ * Returns the RFC 9562 version, from 1 to 8, or 0 for the Nil UUID and 15 for the Max UUID.
102
+ */
103
+ getVersion(): number {
104
+ return Number.parseInt(this.value.charAt(14), 16);
105
+ }
106
+
107
+ /**
108
+ * Returns the moment a version 7 UUID encodes, with millisecond precision.
109
+ *
110
+ * @returns The moment, or `null` for other versions.
111
+ */
112
+ getDate(): Nullable<Date> {
113
+ if (this.getVersion() !== 7) {
114
+ return null;
115
+ }
116
+
117
+ const milliseconds = Number.parseInt(this.value.slice(0, 8) + this.value.slice(9, 13), 16);
118
+ return new Date(milliseconds);
119
+ }
120
+
121
+ equals(other: Uuid): boolean {
122
+ return this.value === other.value;
123
+ }
124
+
125
+ /**
126
+ * Returns the UUID in lowercase canonical form.
127
+ */
128
+ toString(): string {
129
+ return this.value;
130
+ }
131
+
132
+ toJSON(): string {
133
+ return this.value;
134
+ }
135
+
136
+ /**
137
+ * Sets the version and the RFC 9562 variant bits of 16 bytes and wraps them as a UUID.
138
+ *
139
+ * @param bytes Exactly 16 bytes; only the version and variant bits are overwritten.
140
+ * @param version From 1 to 8.
141
+ */
142
+ private static createWithVersion(bytes: Uint8Array, version: number): Uuid {
143
+ bytes[6] = (bytes[6] & 0x0f) | (version << 4);
144
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
145
+
146
+ let hex = '';
147
+ for (const byte of bytes) {
148
+ hex += byte.toString(16).padStart(2, '0');
149
+ }
150
+
151
+ return new Uuid(`${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`);
152
+ }
153
+ }