@naturalcycles/js-lib 15.40.2 → 15.42.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.
@@ -20,7 +20,6 @@ export function _range(fromIncl, toExcl, step = 1) {
20
20
  * If it was an object - it'll paste the same object reference, which can create bugs.
21
21
  */
22
22
  export function _arrayFilled(length, fill) {
23
- // biome-ignore lint/style/useConsistentBuiltinInstantiation: ok
24
23
  return Array(length).fill(fill);
25
24
  }
26
25
  export function _rangeIterable(fromIncl, toExcl, step = 1) {
@@ -101,9 +101,10 @@ export declare class JsonSchemaStringBuilder<T extends string = string, Opt exte
101
101
  languageTag: () => this;
102
102
  countryCode: () => this;
103
103
  currency: () => this;
104
- trim: (trim?: boolean) => this;
105
- toLowerCase: (toLowerCase?: boolean) => this;
106
- toUpperCase: (toUpperCase?: boolean) => this;
104
+ trim(trim?: boolean): this;
105
+ toLowerCase(toLowerCase?: boolean): this;
106
+ toUpperCase(toUpperCase?: boolean): this;
107
+ truncate(toLength: number): this;
107
108
  branded<B extends string>(): JsonSchemaStringBuilder<B>;
108
109
  /**
109
110
  * Accepts only the `YYYY-MM-DD` shape from all ISO 8601 variants.
@@ -115,11 +116,10 @@ export declare class JsonSchemaStringBuilder<T extends string = string, Opt exte
115
116
  */
116
117
  isoDateTime(): JsonSchemaStringBuilder<IsoDateTime>;
117
118
  jwt(): this;
118
- private transformModify;
119
119
  }
120
120
  export declare class JsonSchemaObjectBuilder<T extends AnyObject, Opt extends boolean = false> extends JsonSchemaAnyBuilder<T, JsonSchemaObject<T>, Opt> {
121
121
  constructor();
122
- addProperties(props: {
122
+ addProperties(props?: {
123
123
  [k in keyof T]: JsonSchemaBuilder<T[k]>;
124
124
  }): this;
125
125
  /**
@@ -144,14 +144,14 @@ export declare class JsonSchemaArrayBuilder<ITEM, Opt extends boolean = false> e
144
144
  export declare class JsonSchemaTupleBuilder<T extends any[]> extends JsonSchemaAnyBuilder<T, JsonSchemaTuple<T>> {
145
145
  constructor(items: JsonSchemaBuilder[]);
146
146
  }
147
- declare function object<P extends Record<string, JsonSchemaAnyBuilder<any, any, any>>>(props: P): JsonSchemaObjectBuilder<{
147
+ declare function object<P extends Record<string, JsonSchemaAnyBuilder<any, any, any>>>(props?: P): JsonSchemaObjectBuilder<{
148
148
  [K in keyof P as P[K] extends JsonSchemaAnyBuilder<any, any, infer Opt> ? Opt extends true ? never : K : never]: P[K] extends JsonSchemaAnyBuilder<infer U, any, any> ? U : never;
149
149
  } & {
150
150
  [K in keyof P as P[K] extends JsonSchemaAnyBuilder<any, any, infer Opt> ? Opt extends true ? K : never : never]?: P[K] extends JsonSchemaAnyBuilder<infer U, any, any> ? U : never;
151
151
  } extends infer O ? {
152
152
  [K in keyof O]: O[K];
153
153
  } : never>;
154
- declare function object<T extends AnyObject>(props: {
154
+ declare function object<T extends AnyObject>(props?: {
155
155
  [K in keyof T]: JsonSchemaAnyBuilder<T[K]>;
156
156
  }): JsonSchemaObjectBuilder<T>;
157
157
  export {};
@@ -54,9 +54,7 @@ export const j = {
54
54
  });
55
55
  },
56
56
  buffer() {
57
- return new JsonSchemaAnyBuilder({
58
- instanceof: 'Buffer',
59
- });
57
+ return new JsonSchemaAnyBuilder({ instanceof: 'Buffer' });
60
58
  },
61
59
  // number types
62
60
  number() {
@@ -144,7 +142,7 @@ export class JsonSchemaAnyBuilder {
144
142
  return this;
145
143
  }
146
144
  instanceof(of) {
147
- this.schema.instanceof = of;
145
+ Object.assign(this.schema, { type: 'object', instanceof: of });
148
146
  return this;
149
147
  }
150
148
  optional(optional = true) {
@@ -280,9 +278,22 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
280
278
  languageTag = () => this.format('languageTag');
281
279
  countryCode = () => this.format('countryCode');
282
280
  currency = () => this.format('currency');
283
- trim = (trim = true) => this.transformModify('trim', trim);
284
- toLowerCase = (toLowerCase = true) => this.transformModify('toLowerCase', toLowerCase);
285
- toUpperCase = (toUpperCase = true) => this.transformModify('toUpperCase', toUpperCase);
281
+ trim(trim = true) {
282
+ Object.assign(this.schema, { transform: { ...this.schema.transform, trim } });
283
+ return this;
284
+ }
285
+ toLowerCase(toLowerCase = true) {
286
+ Object.assign(this.schema, { transform: { ...this.schema.transform, toLowerCase } });
287
+ return this;
288
+ }
289
+ toUpperCase(toUpperCase = true) {
290
+ Object.assign(this.schema, { transform: { ...this.schema.transform, toUpperCase } });
291
+ return this;
292
+ }
293
+ truncate(toLength) {
294
+ Object.assign(this.schema, { transform: { ...this.schema.transform, truncate: toLength } });
295
+ return this;
296
+ }
286
297
  branded() {
287
298
  return this;
288
299
  }
@@ -302,15 +313,6 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
302
313
  jwt() {
303
314
  return this.regex(JWT_REGEX);
304
315
  }
305
- transformModify(t, add) {
306
- if (add) {
307
- this.schema.transform = _uniq([...(this.schema.transform || []), t]);
308
- }
309
- else {
310
- this.schema.transform = this.schema.transform?.filter(s => s !== t);
311
- }
312
- return this;
313
- }
314
316
  }
315
317
  export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
316
318
  constructor() {
@@ -322,6 +324,8 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
322
324
  });
323
325
  }
324
326
  addProperties(props) {
327
+ if (!props)
328
+ return this;
325
329
  Object.entries(props).forEach(([k, builder]) => {
326
330
  const schema = builder.build();
327
331
  if (!schema.optionalField) {
@@ -1,4 +1,5 @@
1
1
  export * from './deepEquals.js';
2
+ export * from './keySortedMap.js';
2
3
  export * from './map2.js';
3
4
  export * from './object.util.js';
4
5
  export * from './set2.js';
@@ -1,4 +1,5 @@
1
1
  export * from './deepEquals.js';
2
+ export * from './keySortedMap.js';
2
3
  export * from './map2.js';
3
4
  export * from './object.util.js';
4
5
  export * from './set2.js';
@@ -0,0 +1,77 @@
1
+ export interface KeySortedMapOptions {
2
+ /**
3
+ * Defaults to false.
4
+ * Set to true if your keys are numeric,
5
+ * so it would sort correctly.
6
+ */
7
+ numericKeys?: boolean;
8
+ }
9
+ /**
10
+ * Maintains sorted array of keys.
11
+ * Sorts **on insertion**, not on retrieval.
12
+ *
13
+ * - set(): O(log n) search + O(n) splice only when inserting a NEW key
14
+ * - get/has: O(1)
15
+ * - delete: O(log n) search + O(n) splice if present
16
+ * - iteration: O(n) over pre-sorted keys (no sorting at iteration time)
17
+ *
18
+ * @experimental
19
+ */
20
+ export declare class KeySortedMap<K, V> implements Map<K, V> {
21
+ opt: KeySortedMapOptions;
22
+ private readonly map;
23
+ private readonly sortedKeys;
24
+ constructor(entries?: [K, V][], opt?: KeySortedMapOptions);
25
+ /**
26
+ * Convenience way to create KeySortedMap from object.
27
+ */
28
+ static of<V>(obj: Record<any, V>): KeySortedMap<string, V>;
29
+ get size(): number;
30
+ clear(): void;
31
+ has(key: K): boolean;
32
+ get(key: K): V | undefined;
33
+ /**
34
+ * Allows to set multiple key-value pairs at once.
35
+ */
36
+ setMany(obj: Record<any, V>): this;
37
+ /**
38
+ * Insert or update. Keeps keys array sorted at all times.
39
+ * Returns this (Map-like).
40
+ */
41
+ set(key: K, value: V): this;
42
+ /**
43
+ * Delete by key. Returns boolean like Map.delete.
44
+ */
45
+ delete(key: K): boolean;
46
+ /**
47
+ * Iterables (Map-compatible), all in sorted order.
48
+ */
49
+ keys(): MapIterator<K>;
50
+ values(): MapIterator<V>;
51
+ entries(): MapIterator<[K, V]>;
52
+ [Symbol.iterator](): MapIterator<[K, V]>;
53
+ [Symbol.toStringTag]: string;
54
+ /**
55
+ * Zero-allocation callbacks over sorted data (faster than spreading to arrays).
56
+ */
57
+ forEach(cb: (value: V, key: K, map: Map<K, V>) => void, thisArg?: any): void;
58
+ /**
59
+ * Convenience methods that MATERIALIZE arrays (if you really want arrays).
60
+ * These allocate; use iterators/forEach for maximum performance.
61
+ */
62
+ keysArray(): K[];
63
+ valuesArray(): V[];
64
+ entriesArray(): [K, V][];
65
+ /** Fast helpers */
66
+ firstKey(): K | undefined;
67
+ lastKey(): K | undefined;
68
+ firstEntry(): [K, V] | undefined;
69
+ lastEntry(): [K, V] | undefined;
70
+ toJSON(): Record<string, V>;
71
+ toObject(): Record<string, V>;
72
+ /**
73
+ * lowerBound: first index i s.t. keys[i] >= target
74
+ */
75
+ private lowerBound;
76
+ private sortKeys;
77
+ }
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Maintains sorted array of keys.
3
+ * Sorts **on insertion**, not on retrieval.
4
+ *
5
+ * - set(): O(log n) search + O(n) splice only when inserting a NEW key
6
+ * - get/has: O(1)
7
+ * - delete: O(log n) search + O(n) splice if present
8
+ * - iteration: O(n) over pre-sorted keys (no sorting at iteration time)
9
+ *
10
+ * @experimental
11
+ */
12
+ export class KeySortedMap {
13
+ opt;
14
+ map;
15
+ sortedKeys;
16
+ constructor(entries = [], opt = {}) {
17
+ this.opt = opt;
18
+ this.map = new Map(entries);
19
+ this.sortedKeys = [...this.map.keys()];
20
+ this.sortKeys();
21
+ }
22
+ /**
23
+ * Convenience way to create KeySortedMap from object.
24
+ */
25
+ static of(obj) {
26
+ return new KeySortedMap(Object.entries(obj));
27
+ }
28
+ get size() {
29
+ return this.map.size;
30
+ }
31
+ clear() {
32
+ this.map.clear();
33
+ this.sortedKeys.length = 0;
34
+ }
35
+ has(key) {
36
+ return this.map.has(key);
37
+ }
38
+ get(key) {
39
+ return this.map.get(key);
40
+ }
41
+ /**
42
+ * Allows to set multiple key-value pairs at once.
43
+ */
44
+ setMany(obj) {
45
+ for (const [k, v] of Object.entries(obj)) {
46
+ this.map.set(k, v);
47
+ this.sortedKeys.push(k);
48
+ }
49
+ // Resort all at once
50
+ this.sortKeys();
51
+ return this;
52
+ }
53
+ /**
54
+ * Insert or update. Keeps keys array sorted at all times.
55
+ * Returns this (Map-like).
56
+ */
57
+ set(key, value) {
58
+ if (this.map.has(key)) {
59
+ // Update only; position unchanged.
60
+ this.map.set(key, value);
61
+ return this;
62
+ }
63
+ // Find insertion index (lower_bound).
64
+ const i = this.lowerBound(key);
65
+ // Only insert into keys when actually new.
66
+ this.sortedKeys.splice(i, 0, key);
67
+ this.map.set(key, value);
68
+ return this;
69
+ }
70
+ /**
71
+ * Delete by key. Returns boolean like Map.delete.
72
+ */
73
+ delete(key) {
74
+ if (!this.map.has(key))
75
+ return false;
76
+ this.map.delete(key);
77
+ // Remove from keys using binary search to avoid O(n) find.
78
+ const i = this.lowerBound(key);
79
+ // Because key existed, it must be at i.
80
+ if (i < this.sortedKeys.length && this.sortedKeys[i] === key) {
81
+ this.sortedKeys.splice(i, 1);
82
+ }
83
+ else {
84
+ // Extremely unlikely if external mutation happened; safe guard.
85
+ // Fall back to linear search (shouldn't happen).
86
+ const j = this.sortedKeys.indexOf(key);
87
+ if (j !== -1)
88
+ this.sortedKeys.splice(j, 1);
89
+ }
90
+ return true;
91
+ }
92
+ /**
93
+ * Iterables (Map-compatible), all in sorted order.
94
+ */
95
+ *keys() {
96
+ for (let i = 0; i < this.sortedKeys.length; i++) {
97
+ yield this.sortedKeys[i];
98
+ }
99
+ }
100
+ *values() {
101
+ for (let i = 0; i < this.sortedKeys.length; i++) {
102
+ yield this.map.get(this.sortedKeys[i]);
103
+ }
104
+ }
105
+ *entries() {
106
+ for (let i = 0; i < this.sortedKeys.length; i++) {
107
+ const k = this.sortedKeys[i];
108
+ yield [k, this.map.get(k)];
109
+ }
110
+ }
111
+ [Symbol.iterator]() {
112
+ return this.entries();
113
+ }
114
+ [Symbol.toStringTag] = 'KeySortedMap';
115
+ /**
116
+ * Zero-allocation callbacks over sorted data (faster than spreading to arrays).
117
+ */
118
+ forEach(cb, thisArg) {
119
+ const m = this.map;
120
+ for (let i = 0; i < this.sortedKeys.length; i++) {
121
+ const k = this.sortedKeys[i];
122
+ cb.call(thisArg, m.get(k), k, this);
123
+ }
124
+ }
125
+ /**
126
+ * Convenience methods that MATERIALIZE arrays (if you really want arrays).
127
+ * These allocate; use iterators/forEach for maximum performance.
128
+ */
129
+ keysArray() {
130
+ return this.sortedKeys.slice();
131
+ }
132
+ valuesArray() {
133
+ // oxlint-disable-next-line unicorn/no-new-array
134
+ const a = Array(this.sortedKeys.length);
135
+ for (let i = 0; i < this.sortedKeys.length; i++) {
136
+ a[i] = this.map.get(this.sortedKeys[i]);
137
+ }
138
+ return a;
139
+ }
140
+ entriesArray() {
141
+ // oxlint-disable-next-line unicorn/no-new-array
142
+ const out = Array(this.sortedKeys.length);
143
+ for (let i = 0; i < this.sortedKeys.length; i++) {
144
+ const k = this.sortedKeys[i];
145
+ out[i] = [k, this.map.get(k)];
146
+ }
147
+ return out;
148
+ }
149
+ /** Fast helpers */
150
+ firstKey() {
151
+ return this.sortedKeys[0];
152
+ }
153
+ lastKey() {
154
+ return this.sortedKeys.length ? this.sortedKeys[this.sortedKeys.length - 1] : undefined;
155
+ }
156
+ firstEntry() {
157
+ if (!this.sortedKeys.length)
158
+ return;
159
+ const k = this.sortedKeys[0];
160
+ return [k, this.map.get(k)];
161
+ }
162
+ lastEntry() {
163
+ if (!this.sortedKeys.length)
164
+ return;
165
+ const k = this.sortedKeys[this.sortedKeys.length - 1];
166
+ return [k, this.map.get(k)];
167
+ }
168
+ toJSON() {
169
+ return this.toObject();
170
+ }
171
+ toObject() {
172
+ return Object.fromEntries(this.map);
173
+ }
174
+ /**
175
+ * lowerBound: first index i s.t. keys[i] >= target
176
+ */
177
+ lowerBound(target) {
178
+ let lo = 0;
179
+ let hi = this.sortedKeys.length;
180
+ while (lo < hi) {
181
+ // oxlint-disable-next-line no-bitwise
182
+ const mid = (lo + hi) >>> 1;
183
+ if (this.sortedKeys[mid] < target) {
184
+ lo = mid + 1;
185
+ }
186
+ else {
187
+ hi = mid;
188
+ }
189
+ }
190
+ return lo;
191
+ }
192
+ sortKeys() {
193
+ if (this.opt.numericKeys) {
194
+ ;
195
+ this.sortedKeys.sort(numericAscCompare);
196
+ }
197
+ else {
198
+ // Default sort - fastest for Strings
199
+ this.sortedKeys.sort();
200
+ }
201
+ }
202
+ }
203
+ function numericAscCompare(a, b) {
204
+ return a - b;
205
+ }
@@ -354,7 +354,6 @@ export function _get(obj = {}, path = '') {
354
354
  * Based on: https://stackoverflow.com/a/54733755/4919972
355
355
  */
356
356
  export function _set(obj, path, value) {
357
- // biome-ignore lint/style/useConsistentBuiltinInstantiation: ok
358
357
  if (!obj || Object(obj) !== obj || !path)
359
358
  return obj; // When obj is not an object
360
359
  // If not yet an array, get the keys from the string-path
@@ -366,9 +365,7 @@ export function _set(obj, path, value) {
366
365
  }
367
366
  // oxlint-disable-next-line unicorn/no-array-reduce
368
367
  ;
369
- path.slice(0, -1).reduce((a, c, i) =>
370
- // biome-ignore lint/style/useConsistentBuiltinInstantiation: ok
371
- Object(a[c]) === a[c] // Does the key exist and is its value an object?
368
+ path.slice(0, -1).reduce((a, c, i) => Object(a[c]) === a[c] // Does the key exist and is its value an object?
372
369
  ? // Yes: then follow that path
373
370
  a[c]
374
371
  : // No: create the key. Is the next key a potential array-index?
@@ -44,6 +44,7 @@ export async function pTimeout(fn, opt) {
44
44
  _typeCast(err);
45
45
  // keep original stack
46
46
  err.stack = fakeError.stack.replace('Error: TimeoutError', err.name + ': ' + err.message);
47
+ // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
47
48
  reject(_errorDataAppend(err, opt.errorData));
48
49
  }
49
50
  return;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/js-lib",
3
3
  "type": "module",
4
- "version": "15.40.2",
4
+ "version": "15.42.0",
5
5
  "dependencies": {
6
6
  "tslib": "^2",
7
7
  "undici": "^7",
@@ -13,7 +13,7 @@
13
13
  "@types/semver": "^7",
14
14
  "crypto-js": "^4",
15
15
  "dayjs": "^1",
16
- "@naturalcycles/dev-lib": "18.4.2"
16
+ "@naturalcycles/dev-lib": "20.4.2"
17
17
  },
18
18
  "exports": {
19
19
  ".": "./dist/index.js",
@@ -36,7 +36,6 @@ export function _range(fromIncl: Integer, toExcl?: Integer, step = 1): number[]
36
36
  * If it was an object - it'll paste the same object reference, which can create bugs.
37
37
  */
38
38
  export function _arrayFilled<T extends Primitive>(length: Integer, fill: T): T[] {
39
- // biome-ignore lint/style/useConsistentBuiltinInstantiation: ok
40
39
  return Array(length).fill(fill)
41
40
  }
42
41
 
@@ -101,9 +101,7 @@ export const j = {
101
101
  })
102
102
  },
103
103
  buffer() {
104
- return new JsonSchemaAnyBuilder<Buffer, JsonSchemaAny<Buffer>>({
105
- instanceof: 'Buffer',
106
- })
104
+ return new JsonSchemaAnyBuilder<Buffer, JsonSchemaAny<Buffer>>({ instanceof: 'Buffer' })
107
105
  },
108
106
 
109
107
  // number types
@@ -215,7 +213,7 @@ export class JsonSchemaAnyBuilder<
215
213
  }
216
214
 
217
215
  instanceof(of: string): this {
218
- this.schema.instanceof = of
216
+ Object.assign(this.schema, { type: 'object', instanceof: of })
219
217
  return this
220
218
  }
221
219
 
@@ -395,9 +393,25 @@ export class JsonSchemaStringBuilder<
395
393
  countryCode = (): this => this.format('countryCode')
396
394
  currency = (): this => this.format('currency')
397
395
 
398
- trim = (trim = true): this => this.transformModify('trim', trim)
399
- toLowerCase = (toLowerCase = true): this => this.transformModify('toLowerCase', toLowerCase)
400
- toUpperCase = (toUpperCase = true): this => this.transformModify('toUpperCase', toUpperCase)
396
+ trim(trim = true): this {
397
+ Object.assign(this.schema, { transform: { ...this.schema.transform, trim } })
398
+ return this
399
+ }
400
+
401
+ toLowerCase(toLowerCase = true): this {
402
+ Object.assign(this.schema, { transform: { ...this.schema.transform, toLowerCase } })
403
+ return this
404
+ }
405
+
406
+ toUpperCase(toUpperCase = true): this {
407
+ Object.assign(this.schema, { transform: { ...this.schema.transform, toUpperCase } })
408
+ return this
409
+ }
410
+
411
+ truncate(toLength: number): this {
412
+ Object.assign(this.schema, { transform: { ...this.schema.transform, truncate: toLength } })
413
+ return this
414
+ }
401
415
 
402
416
  branded<B extends string>(): JsonSchemaStringBuilder<B> {
403
417
  return this as unknown as JsonSchemaStringBuilder<B>
@@ -422,15 +436,6 @@ export class JsonSchemaStringBuilder<
422
436
  return this.regex(JWT_REGEX)
423
437
  }
424
438
 
425
- private transformModify(t: 'trim' | 'toLowerCase' | 'toUpperCase', add: boolean): this {
426
- if (add) {
427
- this.schema.transform = _uniq([...(this.schema.transform || []), t])
428
- } else {
429
- this.schema.transform = this.schema.transform?.filter(s => s !== t)
430
- }
431
- return this
432
- }
433
-
434
439
  // contentMediaType?: string
435
440
  // contentEncoding?: string
436
441
  }
@@ -448,7 +453,9 @@ export class JsonSchemaObjectBuilder<
448
453
  })
449
454
  }
450
455
 
451
- addProperties(props: { [k in keyof T]: JsonSchemaBuilder<T[k]> }): this {
456
+ addProperties(props?: { [k in keyof T]: JsonSchemaBuilder<T[k]> }): this {
457
+ if (!props) return this
458
+
452
459
  Object.entries(props).forEach(([k, builder]: [keyof T, JsonSchemaBuilder]) => {
453
460
  const schema = builder.build()
454
461
  if (!schema.optionalField) {
@@ -555,7 +562,7 @@ export class JsonSchemaTupleBuilder<T extends any[]> extends JsonSchemaAnyBuilde
555
562
  }
556
563
 
557
564
  function object<P extends Record<string, JsonSchemaAnyBuilder<any, any, any>>>(
558
- props: P,
565
+ props?: P,
559
566
  ): JsonSchemaObjectBuilder<
560
567
  {
561
568
  [K in keyof P as P[K] extends JsonSchemaAnyBuilder<any, any, infer Opt>
@@ -573,10 +580,10 @@ function object<P extends Record<string, JsonSchemaAnyBuilder<any, any, any>>>(
573
580
  ? { [K in keyof O]: O[K] }
574
581
  : never
575
582
  >
576
- function object<T extends AnyObject>(props: {
583
+ function object<T extends AnyObject>(props?: {
577
584
  [K in keyof T]: JsonSchemaAnyBuilder<T[K]>
578
585
  }): JsonSchemaObjectBuilder<T>
579
586
 
580
- function object(props: any): any {
587
+ function object(props?: any): any {
581
588
  return new JsonSchemaObjectBuilder<any>().addProperties(props)
582
589
  }
@@ -1,4 +1,5 @@
1
1
  export * from './deepEquals.js'
2
+ export * from './keySortedMap.js'
2
3
  export * from './map2.js'
3
4
  export * from './object.util.js'
4
5
  export * from './set2.js'
@@ -0,0 +1,234 @@
1
+ export interface KeySortedMapOptions {
2
+ /**
3
+ * Defaults to false.
4
+ * Set to true if your keys are numeric,
5
+ * so it would sort correctly.
6
+ */
7
+ numericKeys?: boolean
8
+ }
9
+
10
+ /**
11
+ * Maintains sorted array of keys.
12
+ * Sorts **on insertion**, not on retrieval.
13
+ *
14
+ * - set(): O(log n) search + O(n) splice only when inserting a NEW key
15
+ * - get/has: O(1)
16
+ * - delete: O(log n) search + O(n) splice if present
17
+ * - iteration: O(n) over pre-sorted keys (no sorting at iteration time)
18
+ *
19
+ * @experimental
20
+ */
21
+ export class KeySortedMap<K, V> implements Map<K, V> {
22
+ private readonly map: Map<K, V>
23
+ private readonly sortedKeys: K[]
24
+
25
+ constructor(
26
+ entries: [K, V][] = [],
27
+ public opt: KeySortedMapOptions = {},
28
+ ) {
29
+ this.map = new Map(entries)
30
+ this.sortedKeys = [...this.map.keys()]
31
+ this.sortKeys()
32
+ }
33
+
34
+ /**
35
+ * Convenience way to create KeySortedMap from object.
36
+ */
37
+ static of<V>(obj: Record<any, V>): KeySortedMap<string, V> {
38
+ return new KeySortedMap(Object.entries(obj))
39
+ }
40
+
41
+ get size(): number {
42
+ return this.map.size
43
+ }
44
+
45
+ clear(): void {
46
+ this.map.clear()
47
+ this.sortedKeys.length = 0
48
+ }
49
+
50
+ has(key: K): boolean {
51
+ return this.map.has(key)
52
+ }
53
+
54
+ get(key: K): V | undefined {
55
+ return this.map.get(key)
56
+ }
57
+
58
+ /**
59
+ * Allows to set multiple key-value pairs at once.
60
+ */
61
+ setMany(obj: Record<any, V>): this {
62
+ for (const [k, v] of Object.entries(obj)) {
63
+ this.map.set(k as K, v)
64
+ this.sortedKeys.push(k as K)
65
+ }
66
+ // Resort all at once
67
+ this.sortKeys()
68
+ return this
69
+ }
70
+
71
+ /**
72
+ * Insert or update. Keeps keys array sorted at all times.
73
+ * Returns this (Map-like).
74
+ */
75
+ set(key: K, value: V): this {
76
+ if (this.map.has(key)) {
77
+ // Update only; position unchanged.
78
+ this.map.set(key, value)
79
+ return this
80
+ }
81
+ // Find insertion index (lower_bound).
82
+ const i = this.lowerBound(key)
83
+ // Only insert into keys when actually new.
84
+ this.sortedKeys.splice(i, 0, key)
85
+ this.map.set(key, value)
86
+ return this
87
+ }
88
+
89
+ /**
90
+ * Delete by key. Returns boolean like Map.delete.
91
+ */
92
+ delete(key: K): boolean {
93
+ if (!this.map.has(key)) return false
94
+ this.map.delete(key)
95
+ // Remove from keys using binary search to avoid O(n) find.
96
+ const i = this.lowerBound(key)
97
+ // Because key existed, it must be at i.
98
+ if (i < this.sortedKeys.length && this.sortedKeys[i] === key) {
99
+ this.sortedKeys.splice(i, 1)
100
+ } else {
101
+ // Extremely unlikely if external mutation happened; safe guard.
102
+ // Fall back to linear search (shouldn't happen).
103
+ const j = this.sortedKeys.indexOf(key)
104
+ if (j !== -1) this.sortedKeys.splice(j, 1)
105
+ }
106
+ return true
107
+ }
108
+
109
+ /**
110
+ * Iterables (Map-compatible), all in sorted order.
111
+ */
112
+ *keys(): MapIterator<K> {
113
+ for (let i = 0; i < this.sortedKeys.length; i++) {
114
+ yield this.sortedKeys[i]!
115
+ }
116
+ }
117
+
118
+ *values(): MapIterator<V> {
119
+ for (let i = 0; i < this.sortedKeys.length; i++) {
120
+ yield this.map.get(this.sortedKeys[i]!)!
121
+ }
122
+ }
123
+
124
+ *entries(): MapIterator<[K, V]> {
125
+ for (let i = 0; i < this.sortedKeys.length; i++) {
126
+ const k = this.sortedKeys[i]!
127
+ yield [k, this.map.get(k)!]
128
+ }
129
+ }
130
+
131
+ [Symbol.iterator](): MapIterator<[K, V]> {
132
+ return this.entries()
133
+ }
134
+
135
+ [Symbol.toStringTag] = 'KeySortedMap'
136
+
137
+ /**
138
+ * Zero-allocation callbacks over sorted data (faster than spreading to arrays).
139
+ */
140
+ forEach(cb: (value: V, key: K, map: Map<K, V>) => void, thisArg?: any): void {
141
+ const m = this.map
142
+ for (let i = 0; i < this.sortedKeys.length; i++) {
143
+ const k = this.sortedKeys[i]!
144
+ cb.call(thisArg, m.get(k)!, k, this)
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Convenience methods that MATERIALIZE arrays (if you really want arrays).
150
+ * These allocate; use iterators/forEach for maximum performance.
151
+ */
152
+ keysArray(): K[] {
153
+ return this.sortedKeys.slice()
154
+ }
155
+
156
+ valuesArray(): V[] {
157
+ // oxlint-disable-next-line unicorn/no-new-array
158
+ const a = Array<V>(this.sortedKeys.length)
159
+ for (let i = 0; i < this.sortedKeys.length; i++) {
160
+ a[i] = this.map.get(this.sortedKeys[i]!)!
161
+ }
162
+ return a
163
+ }
164
+
165
+ entriesArray(): [K, V][] {
166
+ // oxlint-disable-next-line unicorn/no-new-array
167
+ const out = Array<[K, V]>(this.sortedKeys.length)
168
+ for (let i = 0; i < this.sortedKeys.length; i++) {
169
+ const k = this.sortedKeys[i]!
170
+ out[i] = [k, this.map.get(k)!]
171
+ }
172
+ return out
173
+ }
174
+
175
+ /** Fast helpers */
176
+ firstKey(): K | undefined {
177
+ return this.sortedKeys[0]
178
+ }
179
+
180
+ lastKey(): K | undefined {
181
+ return this.sortedKeys.length ? this.sortedKeys[this.sortedKeys.length - 1] : undefined
182
+ }
183
+
184
+ firstEntry(): [K, V] | undefined {
185
+ if (!this.sortedKeys.length) return
186
+ const k = this.sortedKeys[0]!
187
+ return [k, this.map.get(k)!]
188
+ }
189
+
190
+ lastEntry(): [K, V] | undefined {
191
+ if (!this.sortedKeys.length) return
192
+ const k = this.sortedKeys[this.sortedKeys.length - 1]!
193
+ return [k, this.map.get(k)!]
194
+ }
195
+
196
+ toJSON(): Record<string, V> {
197
+ return this.toObject()
198
+ }
199
+
200
+ toObject(): Record<string, V> {
201
+ return Object.fromEntries(this.map)
202
+ }
203
+
204
+ /**
205
+ * lowerBound: first index i s.t. keys[i] >= target
206
+ */
207
+ private lowerBound(target: K): number {
208
+ let lo = 0
209
+ let hi = this.sortedKeys.length
210
+ while (lo < hi) {
211
+ // oxlint-disable-next-line no-bitwise
212
+ const mid = (lo + hi) >>> 1
213
+ if (this.sortedKeys[mid]! < target) {
214
+ lo = mid + 1
215
+ } else {
216
+ hi = mid
217
+ }
218
+ }
219
+ return lo
220
+ }
221
+
222
+ private sortKeys(): void {
223
+ if (this.opt.numericKeys) {
224
+ ;(this.sortedKeys as number[]).sort(numericAscCompare)
225
+ } else {
226
+ // Default sort - fastest for Strings
227
+ this.sortedKeys.sort()
228
+ }
229
+ }
230
+ }
231
+
232
+ function numericAscCompare(a: number, b: number): number {
233
+ return a - b
234
+ }
@@ -421,7 +421,6 @@ type PropertyPath = Many<PropertyKey>
421
421
  * Based on: https://stackoverflow.com/a/54733755/4919972
422
422
  */
423
423
  export function _set<T extends AnyObject>(obj: T, path: PropertyPath, value: any): T {
424
- // biome-ignore lint/style/useConsistentBuiltinInstantiation: ok
425
424
  if (!obj || Object(obj) !== obj || !path) return obj as any // When obj is not an object
426
425
 
427
426
  // If not yet an array, get the keys from the string-path
@@ -438,7 +437,6 @@ export function _set<T extends AnyObject>(obj: T, path: PropertyPath, value: any
438
437
  c,
439
438
  i, // Iterate all of them except the last one
440
439
  ) =>
441
- // biome-ignore lint/style/useConsistentBuiltinInstantiation: ok
442
440
  Object(a[c]) === a[c] // Does the key exist and is its value an object?
443
441
  ? // Yes: then follow that path
444
442
  a[c]
@@ -88,6 +88,7 @@ export async function pTimeout<T>(fn: AnyAsyncFunction<T>, opt: PTimeoutOptions)
88
88
  _typeCast<Error>(err)
89
89
  // keep original stack
90
90
  err.stack = fakeError.stack!.replace('Error: TimeoutError', err.name + ': ' + err.message)
91
+ // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
91
92
  reject(_errorDataAppend(err, opt.errorData))
92
93
  }
93
94
  return