@oscarpalmer/atoms 0.183.0 → 0.184.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.
package/src/queue.ts CHANGED
@@ -2,6 +2,243 @@ import type {GenericAsyncCallback, GenericCallback} from './models';
2
2
 
3
3
  // #region Types
4
4
 
5
+ type HandleType = 'clear' | 'pause' | 'resume';
6
+
7
+ class KeyedQueue<CallbackParameters extends Parameters<GenericAsyncCallback>, CallbackResult> {
8
+ readonly #callback: GenericAsyncCallback;
9
+
10
+ readonly #options: Required<QueueOptions>;
11
+
12
+ readonly #queues: Map<string, Queue<Tail<CallbackParameters>, CallbackResult>> = new Map();
13
+
14
+ /**
15
+ * Is any queue active?
16
+ */
17
+ get active(): string[] {
18
+ return this.#getStatus(STATUS_ACTIVE);
19
+ }
20
+
21
+ /**
22
+ * Does the queue automatically start when the first item is added?
23
+ */
24
+ get autostart(): boolean {
25
+ return this.#options.autostart;
26
+ }
27
+
28
+ /**
29
+ * Maximum number of runners to process the queue concurrently
30
+ */
31
+ get concurrency(): number {
32
+ return this.#options.concurrency;
33
+ }
34
+
35
+ /**
36
+ * Are all queues empty?
37
+ */
38
+ get empty(): string[] {
39
+ return this.#getStatus(STATUS_EMPTY);
40
+ }
41
+
42
+ /**
43
+ * Are all queues full?
44
+ */
45
+ get full(): string[] {
46
+ return this.#getStatus(STATUS_FULL);
47
+ }
48
+
49
+ /**
50
+ * Number of items in all queues
51
+ */
52
+ get items(): Record<string, number> {
53
+ const size: Record<string, number> = {};
54
+
55
+ const queues = this.#queues.entries();
56
+
57
+ for (const [key, queue] of queues) {
58
+ size[key] = queue.size;
59
+ }
60
+
61
+ return size;
62
+ }
63
+
64
+ /**
65
+ * Keys of all queues
66
+ */
67
+ get keys(): string[] {
68
+ return [...this.#queues.keys()];
69
+ }
70
+
71
+ /**
72
+ * Maximum number of items allowed in the queue
73
+ */
74
+ get maximum(): number {
75
+ return this.#options.maximum;
76
+ }
77
+
78
+ /**
79
+ * Are all queues paused?
80
+ */
81
+ get paused(): string[] {
82
+ return this.#getStatus(STATUS_PAUSED);
83
+ }
84
+
85
+ /**
86
+ * Number of queues
87
+ */
88
+ get queues(): number {
89
+ return this.#queues.size;
90
+ }
91
+
92
+ constructor(callback: GenericAsyncCallback, options: Required<QueueOptions>) {
93
+ this.#callback = callback;
94
+ this.#options = options;
95
+ }
96
+
97
+ /**
98
+ * Queue an item for a specific key
99
+ * @param key Key to queue the item for
100
+ * @param parameters Parameters to use when item runs
101
+ * @param signal Optional signal to abort the item
102
+ * @returns Queued item
103
+ */
104
+ add(
105
+ key: string,
106
+ parameters: Tail<CallbackParameters>,
107
+ signal?: AbortSignal,
108
+ ): Queued<CallbackResult> {
109
+ return this.#getQueue(key, true).add(parameters, signal);
110
+ }
111
+
112
+ /**
113
+ * Clear all items for a specific key _(or all items for all keys, if no key is provided)_
114
+ * @param key Optional key to clear the queue for
115
+ */
116
+ clear(key?: string): void {
117
+ this.#handleQueues(HANDLE_CLEAR, key);
118
+ }
119
+
120
+ /**
121
+ * Get the queue for a specific key
122
+ * @param key Key to get the queue for
123
+ * @returns Queue for the key, or `undefined` if it doesn't exist
124
+ */
125
+ get(key: string): Queue<Tail<CallbackParameters>, CallbackResult> | undefined {
126
+ return this.#getQueue(key);
127
+ }
128
+
129
+ /**
130
+ * Pause the queue for a specific key _(or all queues, if no key is provided)_
131
+ * @param key Optional key to pause the queue for
132
+ */
133
+ pause(key?: string): void {
134
+ this.#handleQueues(HANDLE_PAUSE, key);
135
+ }
136
+
137
+ /**
138
+ * Remove a specific item for a specific key
139
+ * @param key Key to remove the item for
140
+ * @param id ID of the item to remove
141
+ */
142
+ remove(key: string, id: number): void;
143
+
144
+ /**
145
+ * Remove a queue and its items for a specific key
146
+ *
147
+ * _(To remove all items for a specific key, use `clear()` instead)_
148
+ * @param key Key to remove the queue for
149
+ */
150
+ remove(key: string): void;
151
+
152
+ /**
153
+ * Remove all queues and their items
154
+ */
155
+ remove(): void;
156
+
157
+ remove(key?: string, id?: number): void {
158
+ if (key == null) {
159
+ this.#handleQueues(HANDLE_CLEAR);
160
+
161
+ this.#queues.clear();
162
+
163
+ return;
164
+ }
165
+
166
+ const queue = this.#getQueue(key);
167
+
168
+ if (queue == null) {
169
+ return;
170
+ }
171
+
172
+ if (typeof id === 'number') {
173
+ queue.remove(id);
174
+
175
+ return;
176
+ }
177
+
178
+ queue.clear();
179
+
180
+ this.#queues.delete(key);
181
+ }
182
+
183
+ /**
184
+ * Resume the queue for a specific key _(or all queues, if no key is provided)_
185
+ * @param key Optional key to resume the queue for
186
+ */
187
+ resume(key?: string): void {
188
+ this.#handleQueues(HANDLE_RESUME, key);
189
+ }
190
+
191
+ #getQueue(key: string, add: true): Queue<Tail<CallbackParameters>, CallbackResult>;
192
+
193
+ #getQueue(key: string): Queue<Tail<CallbackParameters>, CallbackResult> | undefined;
194
+
195
+ #getQueue(
196
+ key: string,
197
+ add?: boolean,
198
+ ): Queue<Tail<CallbackParameters>, CallbackResult> | undefined {
199
+ if (typeof key !== 'string' || key.trim().length === 0) {
200
+ throw new TypeError(MESSAGE_KEY);
201
+ }
202
+
203
+ let queue = this.#queues.get(key);
204
+
205
+ if (queue == null && add === true) {
206
+ queue = new Queue(this.#callback, this.#options, key);
207
+
208
+ this.#queues.set(key, queue);
209
+ }
210
+
211
+ return queue;
212
+ }
213
+
214
+ #getStatus(status: StatusKey): string[] {
215
+ const queues = this.#queues.entries();
216
+ const result: string[] = [];
217
+
218
+ for (const [key, queue] of queues) {
219
+ if (queue[status]) {
220
+ result.push(key);
221
+ }
222
+ }
223
+
224
+ return result;
225
+ }
226
+
227
+ #handleQueues(type: HandleType, key?: string): void {
228
+ if (typeof key === 'string') {
229
+ this.#getQueue(key)?.[type]();
230
+
231
+ return;
232
+ }
233
+
234
+ const queues = this.#queues.values();
235
+
236
+ for (const queue of queues) {
237
+ queue[type]();
238
+ }
239
+ }
240
+ }
241
+
5
242
  class Queue<CallbackParameters extends Parameters<GenericAsyncCallback>, CallbackResult> {
6
243
  readonly #callback: GenericAsyncCallback;
7
244
 
@@ -11,6 +248,8 @@ class Queue<CallbackParameters extends Parameters<GenericAsyncCallback>, Callbac
11
248
 
12
249
  readonly #items: Array<QueuedItem<CallbackParameters, CallbackResult>> = [];
13
250
 
251
+ readonly #key: string | undefined;
252
+
14
253
  readonly #options: Required<QueueOptions>;
15
254
 
16
255
  #paused: boolean;
@@ -73,8 +312,9 @@ class Queue<CallbackParameters extends Parameters<GenericAsyncCallback>, Callbac
73
312
  return this.#items.length;
74
313
  }
75
314
 
76
- constructor(callback: GenericAsyncCallback, options: Required<QueueOptions>) {
315
+ constructor(callback: GenericAsyncCallback, options: Required<QueueOptions>, key?: string) {
77
316
  this.#callback = callback;
317
+ this.#key = key;
78
318
  this.#options = options;
79
319
 
80
320
  this.#paused = !options.autostart;
@@ -116,6 +356,7 @@ class Queue<CallbackParameters extends Parameters<GenericAsyncCallback>, Callbac
116
356
  parameters,
117
357
  promise,
118
358
  abort: aborter,
359
+ key: this.#key,
119
360
  reject: rejector!,
120
361
  resolve: resolver!,
121
362
  signal: abortSignal,
@@ -214,7 +455,12 @@ class Queue<CallbackParameters extends Parameters<GenericAsyncCallback>, Callbac
214
455
 
215
456
  try {
216
457
  if (!(item.signal?.aborted ?? false)) {
217
- result = await this.#callback(...item.parameters);
458
+ const parameters =
459
+ item.key == null
460
+ ? item.parameters
461
+ : ([item.key, ...item.parameters] as Parameters<GenericAsyncCallback>);
462
+
463
+ result = await this.#callback(...parameters);
218
464
  }
219
465
  } catch (thrown) {
220
466
  error = true;
@@ -277,6 +523,7 @@ type Queued<Value> = {
277
523
  type QueuedItem<CallbackParameters extends Parameters<GenericAsyncCallback>, CallbackResult> = {
278
524
  abort?: () => void;
279
525
  id: number;
526
+ key?: string;
280
527
  parameters: CallbackParameters;
281
528
  promise: Promise<QueuedResult<CallbackResult>>;
282
529
  reject: (reason?: unknown) => void;
@@ -295,6 +542,10 @@ type QueuedResult<Value> = {
295
542
  value: Value;
296
543
  };
297
544
 
545
+ type StatusKey = 'active' | 'empty' | 'full' | 'paused';
546
+
547
+ type Tail<Values extends any[]> = Values extends [infer _, ...infer Rest] ? Rest : never;
548
+
298
549
  // #endregion
299
550
 
300
551
  // #region Functions
@@ -339,13 +590,24 @@ function handleResult<CallbackParameters extends Parameters<GenericAsyncCallback
339
590
  }
340
591
  }
341
592
 
593
+ export function keyedQueue<Callback extends GenericAsyncCallback>(
594
+ callback: Callback,
595
+ options?: QueueOptions,
596
+ ): KeyedQueue<Parameters<Callback>, Awaited<ReturnType<Callback>>> {
597
+ if (typeof callback !== 'function') {
598
+ throw new TypeError(MESSAGE_CALLBACK);
599
+ }
600
+
601
+ return new KeyedQueue(callback, getOptions(options));
602
+ }
603
+
342
604
  /**
343
605
  * Create a queue for an asynchronous callback function
344
606
  * @param callback Callback function for queued items
345
607
  * @param options Queue options
346
608
  * @returns Queue instance
347
609
  */
348
- function queue<Callback extends GenericAsyncCallback>(
610
+ export function queue<Callback extends (key: string, ...parameters: any[]) => Promise<void>>(
349
611
  callback: Callback,
350
612
  options?: QueueOptions,
351
613
  ): Queue<Parameters<Callback>, Awaited<ReturnType<Callback>>>;
@@ -356,12 +618,12 @@ function queue<Callback extends GenericAsyncCallback>(
356
618
  * @param options Queue options
357
619
  * @returns Queue instance
358
620
  */
359
- function queue<Callback extends GenericCallback>(
621
+ export function queue<Callback extends GenericCallback>(
360
622
  callback: Callback,
361
623
  options?: QueueOptions,
362
624
  ): Queue<Parameters<Callback>, ReturnType<Callback>>;
363
625
 
364
- function queue(
626
+ export function queue(
365
627
  callback: GenericCallback,
366
628
  options?: QueueOptions,
367
629
  ): Queue<Parameters<GenericCallback>, ReturnType<GenericCallback>> {
@@ -372,6 +634,8 @@ function queue(
372
634
  return new Queue(callback, getOptions(options));
373
635
  }
374
636
 
637
+ queue.keyed = keyedQueue;
638
+
375
639
  // #endregion
376
640
 
377
641
  // #region Variables
@@ -382,18 +646,41 @@ const EVENT_NAME = 'abort';
382
646
 
383
647
  const EVENT_OPTIONS = {once: true};
384
648
 
649
+ const HANDLE_CLEAR: HandleType = 'clear';
650
+
651
+ const HANDLE_PAUSE: HandleType = 'pause';
652
+
653
+ const HANDLE_RESUME: HandleType = 'resume';
654
+
385
655
  const MESSAGE_CALLBACK = 'A Queue requires a callback function';
386
656
 
387
657
  const MESSAGE_CLEAR = 'Queue was cleared';
388
658
 
659
+ const MESSAGE_KEY = 'Key must be a non-empty string';
660
+
389
661
  const MESSAGE_MAXIMUM = 'Queue has reached its maximum size';
390
662
 
391
663
  const MESSAGE_REMOVE = 'Item removed from queue';
392
664
 
665
+ const STATUS_ACTIVE: StatusKey = 'active';
666
+
667
+ const STATUS_EMPTY: StatusKey = 'empty';
668
+
669
+ const STATUS_FULL: StatusKey = 'full';
670
+
671
+ const STATUS_PAUSED: StatusKey = 'paused';
672
+
393
673
  // #endregion
394
674
 
395
675
  // #region Exports
396
676
 
397
- export {queue, QueueError, type Queue, type Queued, type QueueOptions, type QueuedResult};
677
+ export {
678
+ type KeyedQueue,
679
+ type QueueError,
680
+ type Queue,
681
+ type Queued,
682
+ type QueueOptions,
683
+ type QueuedResult,
684
+ };
398
685
 
399
686
  // #endregion
@@ -88,6 +88,8 @@ function getNormalizeOptions(input?: NormalizeOptions): Options {
88
88
 
89
89
  /**
90
90
  * Initialize a string normalizer
91
+ *
92
+ * Available as `initializeNormalizer` and `normalize.initialize`
91
93
  * @param options Normalization options
92
94
  * @returns Normalizer function
93
95
  */
@@ -0,0 +1,108 @@
1
+ import {isPlainObject} from '../is';
2
+ import type {ArrayOrPlainObject, GenericCallback, PlainObject} from '../models';
3
+
4
+ // #region Types
5
+
6
+ /**
7
+ * A frozen value with readonly properties _(going as deep as possible)_
8
+ */
9
+ export type Frozen<Value extends ArrayOrPlainObject> = {
10
+ readonly [Key in keyof Value]: Value[Key] extends ArrayOrPlainObject
11
+ ? Frozen<Value[Key]>
12
+ : Value[Key];
13
+ };
14
+
15
+ // #endregion
16
+
17
+ // #region Functions
18
+
19
+ /**
20
+ * Freeze an array and all its indices recursively
21
+ * @param array Array to freeze
22
+ * @returns Frozen array
23
+ */
24
+ export function freeze<Item>(array: Item[]): Frozen<Item[]>;
25
+
26
+ /**
27
+ * Freeze a callback
28
+ * @param callback Function to freeze
29
+ * @returns Frozen function
30
+ */
31
+ export function freeze<Fn extends GenericCallback>(fn: Fn): Readonly<Fn>;
32
+
33
+ /**
34
+ * Freeze an object and all its properties recursively
35
+ * @param object Object to freeze
36
+ * @returns Frozen object
37
+ */
38
+ export function freeze<Value extends PlainObject>(object: Value): Frozen<Value>;
39
+
40
+ /**
41
+ * Freeze any value, if possible
42
+ *
43
+ * _(Only arrays, functions, and plain objects are freezable)_
44
+ * @param value Value to freeze
45
+ * @returns Frozen value
46
+ */
47
+ export function freeze<Value>(value: Value): Value;
48
+
49
+ export function freeze(value: unknown): unknown {
50
+ return freezeValue(value, new WeakSet());
51
+ }
52
+
53
+ function freezeArray(array: unknown[], references: WeakSet<any>): Frozen<unknown[]> {
54
+ references.add(array);
55
+
56
+ const {length} = array;
57
+
58
+ for (let index = 0; index < length; index += 1) {
59
+ const value = array[index];
60
+
61
+ if (!references.has(value)) {
62
+ array[index] = freezeValue(array[index], references);
63
+ }
64
+ }
65
+
66
+ return Object.freeze(array) as Frozen<unknown[]>;
67
+ }
68
+
69
+ function freezeFunction(fn: Function, references: WeakSet<any>): Readonly<Function> {
70
+ references.add(fn);
71
+
72
+ return Object.freeze(fn);
73
+ }
74
+
75
+ function freezeObject(object: PlainObject, references: WeakSet<any>): Frozen<PlainObject> {
76
+ references.add(object);
77
+
78
+ const keys = Object.keys(object);
79
+ const {length} = keys;
80
+
81
+ for (let index = 0; index < length; index += 1) {
82
+ const key = keys[index];
83
+
84
+ if (!references.has(object[key])) {
85
+ object[key] = freezeValue(object[key], references);
86
+ }
87
+ }
88
+
89
+ return Object.freeze(object) as Frozen<PlainObject>;
90
+ }
91
+
92
+ function freezeValue(value: unknown, references: WeakSet<any>): unknown {
93
+ switch (true) {
94
+ case typeof value === 'function':
95
+ return freezeFunction(value, references);
96
+
97
+ case Array.isArray(value):
98
+ return freezeArray(value, references);
99
+
100
+ case isPlainObject(value):
101
+ return freezeObject(value, references);
102
+
103
+ default:
104
+ return value;
105
+ }
106
+ }
107
+
108
+ // #endregion
@@ -1,3 +1,4 @@
1
+ export {freeze, type Frozen} from './freeze';
1
2
  export {omit} from './omit';
2
3
  export {pick} from './pick';
3
4
  export {shake, type Shaken} from './shake';
@@ -4,6 +4,22 @@ import type {ArrayOrPlainObject, NestedPartial, PlainObject} from '../models';
4
4
 
5
5
  // #region Types
6
6
 
7
+ /**
8
+ * Options for assigning values
9
+ */
10
+ export type AssignOptions = Omit<MergeOptions, 'assignValues'>;
11
+
12
+ /**
13
+ * Assign values from multiple arrays or objects to the first one
14
+ * @param to Value to assign to
15
+ * @param from Values to assign
16
+ * @returns Assigned value
17
+ */
18
+ export type Assigner<Model extends ArrayOrPlainObject = ArrayOrPlainObject> = (
19
+ to: NestedPartial<Model>,
20
+ from: NestedPartial<Model>[],
21
+ ) => Model;
22
+
7
23
  /**
8
24
  * Options for merging values
9
25
  */
@@ -60,6 +76,27 @@ type ReplaceableObjectsCallback = (name: string) => boolean;
60
76
 
61
77
  // #region Functions
62
78
 
79
+ /**
80
+ * Assign values from multiple arrays or objects to the first one
81
+ * @param to Value to assign to
82
+ * @param from Values to assign
83
+ * @param options Assigning options
84
+ * @returns Assigned value
85
+ */
86
+ export function assign<Model extends ArrayOrPlainObject>(
87
+ to: NestedPartial<Model>,
88
+ from: NestedPartial<Model>[],
89
+ options?: AssignOptions,
90
+ ): Model {
91
+ const actual = getMergeOptions(options);
92
+
93
+ actual.assignValues = true;
94
+
95
+ return mergeValues([to, ...from], actual, true) as Model;
96
+ }
97
+
98
+ assign.initialize = initializeAssigner;
99
+
63
100
  function getMergeOptions(options?: MergeOptions): Options {
64
101
  const actual: Options = {
65
102
  assignValues: false,
@@ -96,6 +133,24 @@ function handleMerge(values: ArrayOrPlainObject[], options: Options): ArrayOrPla
96
133
  return !Array.isArray(values) || values.length === 0 ? {} : mergeValues(values, options, true);
97
134
  }
98
135
 
136
+ /**
137
+ * Create an assigner with predefined options
138
+ *
139
+ * Available as `initializeAssigner` and `assign.initialize`
140
+ * @param options Assigning options
141
+ * @returns Assigner function
142
+ */
143
+ export function initializeAssigner<Model extends ArrayOrPlainObject>(
144
+ options?: AssignOptions,
145
+ ): Assigner<Model> {
146
+ const actual = getMergeOptions(options);
147
+
148
+ actual.assignValues = true;
149
+
150
+ return ((to: ArrayOrPlainObject, from: NestedPartial<ArrayOrPlainObject>[]): ArrayOrPlainObject =>
151
+ mergeValues([to, ...from], actual, true)) as Assigner<Model>;
152
+ }
153
+
99
154
  /**
100
155
  * Create a merger with predefined options
101
156
  *
@@ -139,6 +194,7 @@ export function merge(
139
194
  return handleMerge(values, getMergeOptions(options));
140
195
  }
141
196
 
197
+ merge.assign = assign;
142
198
  merge.initialize = initializeMerger;
143
199
 
144
200
  function mergeObjects(
@@ -11,6 +11,11 @@ export type Shaken<Value extends PlainObject> = {
11
11
 
12
12
  // #region Functions
13
13
 
14
+ /**
15
+ * Shake an object, removing all keys with `undefined` values
16
+ * @param value Object to shake
17
+ * @returns Shaken object
18
+ */
14
19
  export function shake<Value extends PlainObject>(value: Value): Shaken<Value> {
15
20
  const shaken: PlainObject = {};
16
21