@ezez/utils 4.3.0 → 4.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 (176) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/deserialize.d.ts +1 -1
  3. package/dist/deserialize.d.ts.map +1 -1
  4. package/dist/deserialize.js +2 -1
  5. package/dist/deserialize.js.map +1 -1
  6. package/dist/index.d.ts +3 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +3 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/race.d.ts.map +1 -1
  11. package/dist/race.js.map +1 -1
  12. package/dist/serialize.d.ts.map +1 -1
  13. package/dist/serialize.js.map +1 -1
  14. package/dist/serializeToBuffer/const.d.ts +10 -0
  15. package/dist/serializeToBuffer/const.d.ts.map +1 -0
  16. package/dist/serializeToBuffer/const.js +16 -0
  17. package/dist/serializeToBuffer/const.js.map +1 -0
  18. package/dist/serializeToBuffer/serializeToBuffer.d.ts +5 -0
  19. package/dist/serializeToBuffer/serializeToBuffer.d.ts.map +1 -0
  20. package/dist/serializeToBuffer/serializeToBuffer.js +39 -0
  21. package/dist/serializeToBuffer/serializeToBuffer.js.map +1 -0
  22. package/dist/serializeToBuffer/unserializeFromBuffer.d.ts +5 -0
  23. package/dist/serializeToBuffer/unserializeFromBuffer.d.ts.map +1 -0
  24. package/dist/serializeToBuffer/unserializeFromBuffer.js +52 -0
  25. package/dist/serializeToBuffer/unserializeFromBuffer.js.map +1 -0
  26. package/dist/sortBy.d.ts +1 -1
  27. package/dist/sortBy.d.ts.map +1 -1
  28. package/dist/sortBy.js +2 -7
  29. package/dist/sortBy.js.map +1 -1
  30. package/dist/sortByMultiple.d.ts +3 -0
  31. package/dist/sortByMultiple.d.ts.map +1 -0
  32. package/dist/sortByMultiple.js +18 -0
  33. package/dist/sortByMultiple.js.map +1 -0
  34. package/docs/assets/navigation.js +1 -1
  35. package/docs/assets/search.js +1 -1
  36. package/docs/documents/CHANGELOG.html +81 -67
  37. package/docs/functions/index.cap.html +2 -2
  38. package/docs/functions/index.capitalize.html +2 -2
  39. package/docs/functions/index.coalesce.html +2 -2
  40. package/docs/functions/index.compareArrays.html +2 -2
  41. package/docs/functions/index.compareProps.html +2 -2
  42. package/docs/functions/index.deserialize.html +2 -2
  43. package/docs/functions/index.ensureArray.html +2 -2
  44. package/docs/functions/index.ensureDate.html +2 -2
  45. package/docs/functions/index.ensureError.html +2 -2
  46. package/docs/functions/index.ensurePrefix.html +2 -2
  47. package/docs/functions/index.ensureSuffix.html +2 -2
  48. package/docs/functions/index.ensureTimestamp.html +2 -2
  49. package/docs/functions/index.escapeRegExp.html +2 -2
  50. package/docs/functions/index.formatDate.html +2 -2
  51. package/docs/functions/index.get.html +2 -2
  52. package/docs/functions/index.getMultiple.html +2 -2
  53. package/docs/functions/index.insertSeparator.html +2 -2
  54. package/docs/functions/index.isEmpty.html +2 -2
  55. package/docs/functions/index.isNumericString.html +2 -2
  56. package/docs/functions/index.isPlainObject.html +2 -2
  57. package/docs/functions/index.last.html +2 -2
  58. package/docs/functions/index.later-1.html +2 -2
  59. package/docs/functions/index.mapAsync.html +2 -2
  60. package/docs/functions/index.mapValues.html +2 -2
  61. package/docs/functions/index.match.html +2 -2
  62. package/docs/functions/index.memoize.html +2 -2
  63. package/docs/functions/index.merge.html +2 -2
  64. package/docs/functions/index.mostFrequent.html +2 -2
  65. package/docs/functions/index.noop.html +2 -2
  66. package/docs/functions/index.occurrences.html +2 -2
  67. package/docs/functions/index.omit.html +2 -2
  68. package/docs/functions/index.pick.html +2 -2
  69. package/docs/functions/index.pull.html +2 -2
  70. package/docs/functions/index.race.html +6 -3
  71. package/docs/functions/index.remove.html +2 -2
  72. package/docs/functions/index.removeCommonProperties.html +2 -2
  73. package/docs/functions/index.replace.html +2 -2
  74. package/docs/functions/index.replaceDeep.html +2 -2
  75. package/docs/functions/index.rethrow.html +2 -2
  76. package/docs/functions/index.retry.html +2 -2
  77. package/docs/functions/index.round.html +2 -2
  78. package/docs/functions/index.safe.html +2 -2
  79. package/docs/functions/index.sample.html +2 -2
  80. package/docs/functions/index.samples.html +2 -2
  81. package/docs/functions/index.scale.html +2 -2
  82. package/docs/functions/index.seq.html +2 -2
  83. package/docs/functions/index.seqEarlyBreak.html +2 -2
  84. package/docs/functions/index.serialize.html +3 -4
  85. package/docs/functions/index.serializeToBuffer.html +19 -0
  86. package/docs/functions/index.set.html +2 -2
  87. package/docs/functions/index.setImmutable.html +2 -2
  88. package/docs/functions/index.shuffle.html +2 -2
  89. package/docs/functions/index.sortBy.html +4 -4
  90. package/docs/functions/index.sortByMultiple.html +10 -0
  91. package/docs/functions/index.sortProps.html +2 -2
  92. package/docs/functions/index.stripPrefix.html +2 -2
  93. package/docs/functions/index.stripSuffix.html +2 -2
  94. package/docs/functions/index.throttle.html +2 -2
  95. package/docs/functions/index.toggle.html +2 -2
  96. package/docs/functions/index.trim.html +2 -2
  97. package/docs/functions/index.trimEnd.html +2 -2
  98. package/docs/functions/index.trimStart.html +2 -2
  99. package/docs/functions/index.truthy.html +2 -2
  100. package/docs/functions/index.unique.html +2 -2
  101. package/docs/functions/index.unserializeFromBuffer.html +8 -0
  102. package/docs/functions/index.wait.html +2 -2
  103. package/docs/functions/index.waitFor.html +2 -2
  104. package/docs/functions/index.waitSync.html +2 -2
  105. package/docs/index.html +2 -2
  106. package/docs/interfaces/index.ComparePropsOptions.html +3 -3
  107. package/docs/interfaces/index.GetMultipleSource.html +2 -2
  108. package/docs/interfaces/index.GetSource.html +2 -2
  109. package/docs/interfaces/index.IsNumericStringOptions.html +2 -2
  110. package/docs/interfaces/index.OccurencesOptions.html +2 -2
  111. package/docs/interfaces/index.SetImmutableSource.html +2 -2
  112. package/docs/interfaces/index.SetSource.html +2 -2
  113. package/docs/interfaces/index.ThrottleOptions.html +3 -3
  114. package/docs/interfaces/index.ThrottledFunctionExtras.html +3 -3
  115. package/docs/modules/index.html +7 -2
  116. package/docs/modules.html +2 -2
  117. package/docs/types/index.CustomDeserializers.html +1 -1
  118. package/docs/types/index.CustomSerializers.html +1 -1
  119. package/docs/types/index.Later.html +2 -2
  120. package/docs/types/index.MapValuesFn.html +2 -2
  121. package/docs/types/index.MatchCallback.html +1 -1
  122. package/docs/types/index.MergeTwo.html +2 -2
  123. package/docs/types/index.SeqEarlyBreaker.html +2 -2
  124. package/docs/types/index.SeqFn.html +2 -2
  125. package/docs/types/index.SeqFunctions.html +2 -2
  126. package/docs/types/index.SetImmutablePath.html +2 -2
  127. package/docs/types/index.ThrottledFunction.html +1 -1
  128. package/docs/variables/index.mapValuesUNSET.html +2 -2
  129. package/docs/variables/index.mergeUNSET.html +2 -2
  130. package/esm/deserialize.d.ts +1 -1
  131. package/esm/deserialize.d.ts.map +1 -1
  132. package/esm/deserialize.js +1 -1
  133. package/esm/deserialize.js.map +1 -1
  134. package/esm/index.d.ts +3 -0
  135. package/esm/index.d.ts.map +1 -1
  136. package/esm/index.js +3 -0
  137. package/esm/index.js.map +1 -1
  138. package/esm/race.d.ts.map +1 -1
  139. package/esm/race.js.map +1 -1
  140. package/esm/serialize.d.ts.map +1 -1
  141. package/esm/serialize.js.map +1 -1
  142. package/esm/serializeToBuffer/const.d.ts +10 -0
  143. package/esm/serializeToBuffer/const.d.ts.map +1 -0
  144. package/esm/serializeToBuffer/const.js +10 -0
  145. package/esm/serializeToBuffer/const.js.map +1 -0
  146. package/esm/serializeToBuffer/serializeToBuffer.d.ts +5 -0
  147. package/esm/serializeToBuffer/serializeToBuffer.d.ts.map +1 -0
  148. package/esm/serializeToBuffer/serializeToBuffer.js +36 -0
  149. package/esm/serializeToBuffer/serializeToBuffer.js.map +1 -0
  150. package/esm/serializeToBuffer/unserializeFromBuffer.d.ts +5 -0
  151. package/esm/serializeToBuffer/unserializeFromBuffer.d.ts.map +1 -0
  152. package/esm/serializeToBuffer/unserializeFromBuffer.js +48 -0
  153. package/esm/serializeToBuffer/unserializeFromBuffer.js.map +1 -0
  154. package/esm/sortBy.d.ts +1 -1
  155. package/esm/sortBy.d.ts.map +1 -1
  156. package/esm/sortBy.js +2 -7
  157. package/esm/sortBy.js.map +1 -1
  158. package/esm/sortByMultiple.d.ts +3 -0
  159. package/esm/sortByMultiple.d.ts.map +1 -0
  160. package/esm/sortByMultiple.js +15 -0
  161. package/esm/sortByMultiple.js.map +1 -0
  162. package/package.json +1 -1
  163. package/src/deserialize.ts +1 -1
  164. package/src/index.ts +3 -0
  165. package/src/race.ts +1 -0
  166. package/src/serialize.ts +1 -2
  167. package/src/serializeToBuffer/const.ts +16 -0
  168. package/src/serializeToBuffer/serialization.spec.ts +61 -0
  169. package/src/serializeToBuffer/serializeToBuffer.spec.ts +43 -0
  170. package/src/serializeToBuffer/serializeToBuffer.ts +90 -0
  171. package/src/serializeToBuffer/unserializeFromBuffer.spec.ts +63 -0
  172. package/src/serializeToBuffer/unserializeFromBuffer.ts +75 -0
  173. package/src/sortBy.spec.ts +13 -0
  174. package/src/sortBy.ts +11 -15
  175. package/src/sortByMultiple.spec.ts +87 -0
  176. package/src/sortByMultiple.ts +57 -0
@@ -0,0 +1,63 @@
1
+ import must from "must"; // eslint-disable-line @typescript-eslint/no-shadow
2
+
3
+ import { unserializeFromBuffer } from "./unserializeFromBuffer";
4
+
5
+ const unserialize = unserializeFromBuffer.bind(null, Buffer, []);
6
+
7
+ describe("unserialize", () => {
8
+ it("unserializes basic string", async () => {
9
+ must(unserialize(Buffer.from([0x34, 0x66, 0x00, 0x74, 0x65, 0x73, 0x74, 0x00]))).eql(["test"]);
10
+ });
11
+
12
+ it("unserializes multiple strings", async () => {
13
+ must(unserialize(Buffer.from([
14
+ 0x34, 0x66, 0x00, 0x74, 0x65, 0x73, 0x74, 0x00,
15
+ 0x34, 0x66, 0x00, 0x74, 0x65, 0x73, 0x74, 0x00,
16
+ ]))).eql(["test", "test"]);
17
+ });
18
+
19
+ it("unserializes buffer", async () => {
20
+ const result = unserialize(Buffer.from([0x34, 0x74, 0x00, 0x74, 0x65, 0x73, 0x74, 0x00]));
21
+ must(result.length).equal(1);
22
+ must(result[0]).instanceof(Buffer);
23
+ must((result[0] as Buffer).toString()).equal("test");
24
+ });
25
+
26
+ it("unserializes both buffer and string", async () => {
27
+ const result = unserialize(Buffer.from([
28
+ 0x34, 0x74, 0x00, 0x74, 0x65, 0x73, 0x74, 0x00,
29
+ 0x34, 0x66, 0x00, 0x74, 0x65, 0x73, 0x74, 0x00,
30
+ ]));
31
+
32
+ must(result.length).equal(2);
33
+
34
+ must(result[0]).instanceof(Buffer);
35
+ must((result[0] as Buffer).toString()).equal("test");
36
+
37
+ must(result[1]).equal("test");
38
+ });
39
+
40
+ it("unserializes JSON-looking string as string", async () => {
41
+ must(unserialize(
42
+ Buffer.from([0x36, 0x66, 0x00, 0x22, 0x74, 0x65, 0x73, 0x74, 0x22, 0x00]),
43
+ )).eql([`"test"`]);
44
+ });
45
+
46
+ it("unserializes JSON-serialized string as string", async () => {
47
+ must(unserialize(Buffer.from([
48
+ 0x38, 0x66, 0x00, 0x22, 0x73, 0x3a, 0x74, 0x65, 0x73, 0x74, 0x22, 0x00,
49
+ ]))).eql([`"s:test"`]);
50
+ });
51
+
52
+ it("unserializes JSON-serialized json as string", async () => {
53
+ must(unserialize(Buffer.from([
54
+ 0x38, 0x6a, 0x00, 0x22, 0x73, 0x3a, 0x74, 0x65, 0x73, 0x74, 0x22, 0x00,
55
+ ]))).eql(["test"]);
56
+ });
57
+
58
+ it("unserializes number as number", async () => {
59
+ must(unserialize(Buffer.from([
60
+ 0x35, 0x6a, 0x00, 0x22, 0x6e, 0x3a, 0x31, 0x22, 0x00,
61
+ ]))).eql([1]);
62
+ });
63
+ });
@@ -0,0 +1,75 @@
1
+ import { deserialize } from "../deserialize.js";
2
+
3
+ import { BINARY_MARK_BIN, BINARY_MARK_MAP, BINARY_MARK_STRING } from "./const.js";
4
+
5
+ const MAX_DATA_PARTS = 4;
6
+
7
+ const NOT_FOUND = -1;
8
+ const LAST_CHAR = -1;
9
+
10
+ type DeserializeArgs = Parameters<typeof deserialize> extends [any, ...infer Rest] ? Rest : never; // eslint-disable-line @typescript-eslint/no-explicit-any
11
+
12
+ /**
13
+ * Unserialize data from a Buffer serialized with {@link serializeToBuffer}, useful when working with web sockets or
14
+ * other binary protocols. Make sure to understand how {@link deserialize} works before using this function.
15
+ * @param BufferImplementation - Buffer implementation, in browsers use `buffer` npm package
16
+ * and import it from `buffer/`, in Node.js simply pass `Buffer`.
17
+ * @param deserializeArgs - [customDeserializers] - optional arguments if you need to unserialize custom data types,
18
+ * @param rawData - Buffer to unserialize
19
+ * @returns unserialized data
20
+ */
21
+ const unserializeFromBuffer = <RT extends any[] = unknown[]>( // eslint-disable-line @typescript-eslint/no-explicit-any,max-statements
22
+ BufferImplementation: typeof Buffer, deserializeArgs: DeserializeArgs, rawData: Buffer | Uint8Array,
23
+ ): RT => {
24
+ const intData: Uint8Array = rawData instanceof Uint8Array ? rawData : new Uint8Array(rawData);
25
+
26
+ if (intData.length === 0) {
27
+ throw new Error("No data to unserialize");
28
+ }
29
+
30
+ let startPoint = 0;
31
+ const result = [];
32
+
33
+ let i = 0;
34
+ while (i++ < MAX_DATA_PARTS) {
35
+ const dataSplitPoint = intData.indexOf(0, startPoint); // find null
36
+ if (dataSplitPoint === NOT_FOUND) { // no null found = no data
37
+ break;
38
+ }
39
+
40
+ const lengthBytes = BufferImplementation.from(intData.slice(startPoint, dataSplitPoint)).toString("utf8");
41
+ const binaryMark = lengthBytes.slice(LAST_CHAR) as keyof typeof BINARY_MARK_MAP;
42
+ if (!Object.keys(BINARY_MARK_MAP).includes(binaryMark)) {
43
+ throw new Error(`Invalid binary mark: ${binaryMark}`);
44
+ }
45
+
46
+ const len = Number(lengthBytes.slice(0, lengthBytes.length - 1));
47
+
48
+ const dataStringFrom = dataSplitPoint + 1;
49
+ const dataStringTo = dataStringFrom + len;
50
+ const dataBytes = intData.slice(dataStringFrom, dataStringTo);
51
+
52
+ if (binaryMark === BINARY_MARK_BIN) {
53
+ result.push(dataBytes);
54
+ }
55
+ else if (binaryMark === BINARY_MARK_STRING) {
56
+ const stringData = BufferImplementation.from(dataBytes).toString("utf8");
57
+ result.push(stringData);
58
+ }
59
+ else {
60
+ const stringData = BufferImplementation.from(dataBytes).toString("utf8");
61
+ const jsonData = deserialize(stringData, ...deserializeArgs);
62
+ result.push(jsonData);
63
+ }
64
+
65
+ startPoint = dataStringTo + 1; // skip separating null
66
+ }
67
+
68
+ if (result.length === 0) {
69
+ throw new Error("No data found (no split points)");
70
+ }
71
+
72
+ return result as RT;
73
+ };
74
+
75
+ export { unserializeFromBuffer, unserializeFromBuffer as deserializeFromBuffer };
@@ -53,4 +53,17 @@ describe("sortBy", () => {
53
53
  { size: 90 },
54
54
  ]);
55
55
  });
56
+
57
+ it("supports using default value", () => {
58
+ const sizes = [
59
+ { size: 30 },
60
+ { size: 90 },
61
+ {},
62
+ ];
63
+ sizes.sort(sortBy("size", true, 55)).must.eql([
64
+ { size: 30 },
65
+ {}, // size assumed to be 55
66
+ { size: 90 },
67
+ ]);
68
+ });
56
69
  });
package/src/sortBy.ts CHANGED
@@ -1,22 +1,18 @@
1
+ import { sortByMultiple } from "./sortByMultiple.js";
2
+
1
3
  /**
2
4
  * Returns a function that can be used as a callback to `.sort()` method. Returned function will sort array by given
3
5
  * property.
4
- * @param {string} propertyName - name of the property to sort by
5
- * @param {boolean} [asc=true] - should sort be ascending?
6
- * @param {*} defaultValue - value to use when property is not defined in one of the objects
6
+ *
7
+ * @param propertyName - name of the property to sort by
8
+ * @param asc - should sort be ascending?
9
+ * @param defaultValue - value to use when property is not defined in one of the objects
7
10
  */
8
- const sortBy = <T>(propertyName: keyof T, asc = true, defaultValue: unknown = null) => (a: T, b: T) => {
9
- if (a[propertyName] === b[propertyName]) {
10
- return 0;
11
- }
12
- // @ts-expect-error We don't care about types here, it's for runtime pure JS too
13
-
14
- if ((a[propertyName] ?? defaultValue) > (b[propertyName] ?? defaultValue)) {
15
- // eslint-disable-next-line @typescript-eslint/no-magic-numbers
16
- return asc ? 1 : -1;
17
- }
18
- // eslint-disable-next-line @typescript-eslint/no-magic-numbers
19
- return asc ? -1 : 1;
11
+ const sortBy = <T extends Record<string | number | symbol, any>>( // eslint-disable-line @typescript-eslint/no-explicit-any
12
+ propertyName: keyof T, asc = true, defaultValue: unknown = null,
13
+ ) => (a: T, b: T) => {
14
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
15
+ return sortByMultiple([propertyName], asc, { [propertyName]: defaultValue } as Partial<Record<keyof T, any>>)(a, b);
20
16
  };
21
17
 
22
18
  export {
@@ -0,0 +1,87 @@
1
+ import { sortByMultiple } from "./sortByMultiple.js";
2
+
3
+ describe("sortByMultiple", () => {
4
+ const items = [
5
+ { height: 480, bitrate: 300 },
6
+ { height: 720, bitrate: 100 },
7
+ { height: 720, bitrate: 200 },
8
+ { height: 720, bitrate: 50 },
9
+ ];
10
+
11
+ it("must sort asc", () => {
12
+ items.sort(sortByMultiple(["height", "bitrate"])).must.eql([
13
+ { height: 480, bitrate: 300 },
14
+ { height: 720, bitrate: 50 },
15
+ { height: 720, bitrate: 100 },
16
+ { height: 720, bitrate: 200 },
17
+ ]);
18
+ });
19
+
20
+ it("must sort desc", () => {
21
+ items.sort(sortByMultiple(["height", "bitrate"], false)).must.eql([
22
+ { height: 720, bitrate: 200 },
23
+ { height: 720, bitrate: 100 },
24
+ { height: 720, bitrate: 50 },
25
+ { height: 480, bitrate: 300 },
26
+ ]);
27
+ });
28
+
29
+ it("must sort with default value (example 1)", () => {
30
+ const itemsMissing = [
31
+ { height: 480, bitrate: 300 },
32
+ { height: 720, bitrate: 100 },
33
+ { height: 720 },
34
+ { height: 720, bitrate: 200 },
35
+ ];
36
+
37
+ itemsMissing.sort(sortByMultiple(["height", "bitrate"], false, { bitrate: 999 })).must.eql([
38
+ { height: 720 }, // bitrate assumed to be 999
39
+ { height: 720, bitrate: 200 },
40
+ { height: 720, bitrate: 100 },
41
+ { height: 480, bitrate: 300 },
42
+ ]);
43
+ });
44
+
45
+ it("must sort with default value (example 2)", () => {
46
+ const itemsMissing = [
47
+ { height: 480, bitrate: 300 },
48
+ { height: 720, bitrate: 100 },
49
+ { height: 720 },
50
+ { height: 720, bitrate: 200 },
51
+ ];
52
+
53
+ itemsMissing.sort(sortByMultiple(["height", "bitrate"], true, { bitrate: 1 })).must.eql([
54
+ { height: 480, bitrate: 300 },
55
+ { height: 720 }, // bitrate assumed to be 1
56
+ { height: 720, bitrate: 100 },
57
+ { height: 720, bitrate: 200 },
58
+ ]);
59
+ });
60
+
61
+ it("must sort each prop order separately", () => {
62
+ items.sort(sortByMultiple(["height", "bitrate"], { height: true, bitrate: false })).must.eql([
63
+ { height: 480, bitrate: 300 },
64
+ { height: 720, bitrate: 200 },
65
+ { height: 720, bitrate: 100 },
66
+ { height: 720, bitrate: 50 },
67
+ ]);
68
+ });
69
+
70
+ it("must sort each prop order separately (with missing)", () => {
71
+ const itemsMissing = [
72
+ { height: 480, bitrate: 300 },
73
+ { height: 720, bitrate: 100 },
74
+ { height: 720 },
75
+ { height: 720, bitrate: 200 },
76
+ ];
77
+
78
+ itemsMissing.sort(
79
+ sortByMultiple(["height", "bitrate"], { height: true, bitrate: false }, { bitrate: 150 }),
80
+ ).must.eql([
81
+ { height: 480, bitrate: 300 },
82
+ { height: 720, bitrate: 200 },
83
+ { height: 720 }, // bitrate assumed to be 150
84
+ { height: 720, bitrate: 100 },
85
+ ]);
86
+ });
87
+ });
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Returns a function that can be used as a callback to `.sort()` method. Returned function will sort array by given
3
+ * properties.
4
+ *
5
+ * @param propertyNames - name of the properties to sort by, in order of priority
6
+ * @param ascending - should sort be ascending? Pass boolean or object with boolean values for each property
7
+ * @param defaultValues - values to use when properties are not defined in one of the objects
8
+ *
9
+ * @example:
10
+ * ```
11
+ * const videos = [
12
+ * { height: 480, bitrate: 300 },
13
+ * { height: 720, bitrate: 100 },
14
+ * { height: 720 },
15
+ * { height: 720, bitrate: 200 },
16
+ * ];
17
+ * // Sort by height ascending, then by bitrate descending, and set default bitrate to 150
18
+ * itemsMissing.sort(sortByMultiple(["height", "bitrate"], { height: true, bitrate: false }, { bitrate: 150 }));
19
+ * // Result:
20
+ * [
21
+ * { height: 480, bitrate: 300 },
22
+ * { height: 720, bitrate: 200 },
23
+ * { height: 720 }, // bitrate assumed to be 150
24
+ * { height: 720, bitrate: 100 },
25
+ * ]
26
+ * ```
27
+ */
28
+ const sortByMultiple = <
29
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
+ T extends Record<string | number | symbol, any>,
31
+ K extends (keyof T)[],
32
+ >(
33
+ propertyNames: K,
34
+ ascending: boolean | Record<K[number], boolean> = true,
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
+ defaultValues?: Partial<Record<K[number], any>>, // Force defaultValues to use only keys from K
37
+ ): ((a: T, b: T) => number) => (a: T, b: T) => {
38
+ for (const propertyName of propertyNames) {
39
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
40
+ const asc = typeof ascending === "boolean" ? ascending : (ascending[propertyName] ?? true);
41
+ const aBiggerValue = asc ? 1 : -1; // eslint-disable-line @typescript-eslint/no-magic-numbers
42
+ const bBiggerValue = asc ? -1 : 1; // eslint-disable-line @typescript-eslint/no-magic-numbers
43
+
44
+ const aValue = a[propertyName] ?? defaultValues?.[propertyName];
45
+ const bValue = b[propertyName] ?? defaultValues?.[propertyName];
46
+
47
+ if (aValue !== bValue) {
48
+ // @ts-expect-error Too dynamic for TS
49
+ return (aValue > bValue ? aBiggerValue : bBiggerValue);
50
+ }
51
+ }
52
+ return 0;
53
+ };
54
+
55
+ export {
56
+ sortByMultiple,
57
+ };