@axi-engine/utils 0.2.8 → 0.2.10

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/dist/index.d.mts CHANGED
@@ -37,6 +37,22 @@ type PathType = string | string[];
37
37
  * console.log(userInstance.timestamp); // Logs the current date
38
38
  */
39
39
  type Constructor<T = {}> = new (...args: any[]) => T;
40
+ /**
41
+ * Represents an object that has a lifecycle and requires explicit destruction.
42
+ */
43
+ interface Destroyable {
44
+ /**
45
+ * Destroys the object, releasing all held resources.
46
+ * After calling this, the object should be considered unusable.
47
+ */
48
+ destroy(): void;
49
+ }
50
+ /**
51
+ * Describes an object that can be unsubscribed from.
52
+ */
53
+ interface Unsubscribable {
54
+ unsubscribe(): void;
55
+ }
40
56
  /**
41
57
  * Defines the public, read-only contract for an event emitter.
42
58
  * It allows subscribing to an event but not emitting it.
@@ -48,7 +64,7 @@ type Subscribable<T extends any[]> = {
48
64
  * Subscribes a listener to this event.
49
65
  * @returns A function to unsubscribe the listener.
50
66
  */
51
- subscribe(listener: (...args: T) => void): () => void;
67
+ subscribe(listener: (...args: T) => void): Unsubscribable;
52
68
  unsubscribe(listener: (...args: T) => void): boolean;
53
69
  clear(): void;
54
70
  };
@@ -161,6 +177,7 @@ declare function throwIfEmpty<T>(value: T, exceptionMessage: string): asserts va
161
177
  */
162
178
  declare function throwError(message: string): never;
163
179
 
180
+ /** todo: rename to 'utils config' */
164
181
  interface AxiEngineConfig {
165
182
  pathSeparator: string;
166
183
  }
@@ -225,12 +242,44 @@ interface DataSink {
225
242
  * @param path The path to the value to be deleted.
226
243
  */
227
244
  delete(path: PathType): void;
245
+ /**
246
+ * Deletes all values
247
+ */
248
+ clear(): void;
228
249
  }
229
250
  /**
230
251
  * A full CRUD contract for systems that provide complete data management.
231
252
  * Combines both reading and writing capabilities.
232
253
  */
233
- interface DataStorage extends DataSource, DataSink {
254
+ interface DataStorage extends DataSource, DataSink, Destroyable {
255
+ }
256
+
257
+ /**
258
+ * Represents a disposable resource, such as the execution of an Observable or an Event Listener.
259
+ * Allows grouping multiple teardown logic into a single unit (Composite Subscription).
260
+ */
261
+ declare class Subscription implements Unsubscribable {
262
+ private _closed;
263
+ private _teardowns;
264
+ /**
265
+ * Indicates whether this subscription has already been unsubscribed.
266
+ */
267
+ get closed(): boolean;
268
+ /**
269
+ * @param teardown Optional initial teardown logic to execute when unsubscribed.
270
+ */
271
+ constructor(teardown?: () => void);
272
+ /**
273
+ * Adds a teardown logic to this subscription.
274
+ * If the subscription is already closed, the teardown is executed immediately.
275
+ * @param teardown A function or another Unsubscribable object to be managed.
276
+ */
277
+ add(teardown: Unsubscribable | (() => void)): void;
278
+ /**
279
+ * Disposes the resources held by the subscription.
280
+ * Executes all attached teardown logic and clears the list.
281
+ */
282
+ unsubscribe(): void;
234
283
  }
235
284
 
236
285
  /**
@@ -246,9 +295,15 @@ declare class Emitter<T extends any[]> implements Subscribable<T> {
246
295
  get listenerCount(): number;
247
296
  /**
248
297
  * Subscribes a listener to this event.
249
- * @returns A function to unsubscribe the listener.
298
+ * @returns A Subscription object to manage the unsubscription.
299
+ */
300
+ subscribe(listener: (...args: T) => void): Subscription;
301
+ /**
302
+ * Subscribes a listener that triggers only once and then automatically unsubscribes.
303
+ * @param listener The callback function to execute once.
304
+ * @returns A Subscription object (can be used to cancel before the event fires).
250
305
  */
251
- subscribe(listener: (...args: T) => void): () => void;
306
+ once(listener: (...args: T) => void): Subscription;
252
307
  /**
253
308
  * Manually unsubscribe by listener
254
309
  * @returns returns true if an listener has been removed, or false if the listener does not exist.
@@ -263,19 +318,32 @@ declare class Emitter<T extends any[]> implements Subscribable<T> {
263
318
  */
264
319
  clear(): void;
265
320
  }
321
+
266
322
  /**
267
323
  * An Emitter that stores the last emitted value.
268
324
  * New subscribers immediately receive the last value upon subscription.
269
325
  */
270
326
  declare class StateEmitter<T extends any[]> extends Emitter<T> {
271
327
  private _lastValue;
328
+ /**
329
+ * @param initialValue Optional initial value to set.
330
+ */
272
331
  constructor(initialValue?: T);
273
332
  /**
274
333
  * Gets the current value synchronously without subscribing.
275
334
  */
276
335
  get value(): T | undefined;
336
+ /**
337
+ * Updates the state and notifies all listeners.
338
+ * @param args The new value(s).
339
+ */
277
340
  emit(...args: T): void;
278
- subscribe(listener: (...args: T) => void): () => void;
341
+ /**
342
+ * Subscribes to the event. If a value exists, the listener is called immediately.
343
+ * @param listener The callback function.
344
+ * @returns A Subscription object.
345
+ */
346
+ subscribe(listener: (...args: T) => void): Subscription;
279
347
  clear(): void;
280
348
  }
281
349
 
@@ -362,15 +430,18 @@ declare function clampNumber(val: number, min?: number | null, max?: number | nu
362
430
  * Calculates a percentage of a given value.
363
431
  * @param val The base value.
364
432
  * @param percents The percentage to get.
433
+ * @param precision Optional number of decimal places to round the result to.
365
434
  * @returns The calculated percentage of the value.
366
- * @example getPercentOf(200, 10); // returns 20
435
+ * @example getPercentOf(200, 12.5); // returns 25
436
+ * @example getPercentOf(100, 33.333, 2); // returns 33.33
367
437
  */
368
- declare function getPercentOf(val: number, percents: number): number;
438
+ declare function getPercentOf(val: number, percents: number, precision?: number): number;
369
439
 
370
440
  /**
371
441
  * Returns the first key of an object.
372
442
  * @param obj The object from which to get the key.
373
443
  * @returns The first key of the object as a string.
444
+ * @throws {Error} If the argument is not a valid object.
374
445
  */
375
446
  declare function firstKeyOf(obj: any): string;
376
447
 
@@ -395,7 +466,7 @@ declare function randInt(min: number, max: number): number;
395
466
  * Generates a unique identifier using uuidv4.
396
467
  * @returns A unique string ID.
397
468
  */
398
- declare function randId(): string;
469
+ declare function uid(): string;
399
470
 
400
471
  /**
401
472
  * A generic, type-safe wrapper around a Map for managing collections of items by key.
@@ -439,4 +510,4 @@ declare class Registry<K extends PropertyKey, V> {
439
510
  clear(): void;
440
511
  }
441
512
 
442
- export { type AxiEngineConfig, type Constructor, type DataSink, type DataSource, type DataStorage, Emitter, type PathType, Registry, type ScalarType, StateEmitter, type Subscribable, areArraysEqual, axiSettings, clampNumber, configure, ensurePathArray, ensurePathString, firstKeyOf, genArray, getPercentOf, getRandomElement, haveSameElements, isBoolean, isFunction, isNull, isNullOrUndefined, isNumber, isObject, isPercentageString, isPromise, isScalar, isSequentialStart, isString, isUndefined, last, randId, randInt, shuffleArray, throwError, throwIf, throwIfEmpty, unique };
513
+ export { type AxiEngineConfig, type Constructor, type DataSink, type DataSource, type DataStorage, type Destroyable, Emitter, type PathType, Registry, type ScalarType, StateEmitter, type Subscribable, Subscription, type Unsubscribable, areArraysEqual, axiSettings, clampNumber, configure, ensurePathArray, ensurePathString, firstKeyOf, genArray, getPercentOf, getRandomElement, haveSameElements, isBoolean, isFunction, isNull, isNullOrUndefined, isNumber, isObject, isPercentageString, isPromise, isScalar, isSequentialStart, isString, isUndefined, last, randInt, shuffleArray, throwError, throwIf, throwIfEmpty, uid, unique };
package/dist/index.d.ts CHANGED
@@ -37,6 +37,22 @@ type PathType = string | string[];
37
37
  * console.log(userInstance.timestamp); // Logs the current date
38
38
  */
39
39
  type Constructor<T = {}> = new (...args: any[]) => T;
40
+ /**
41
+ * Represents an object that has a lifecycle and requires explicit destruction.
42
+ */
43
+ interface Destroyable {
44
+ /**
45
+ * Destroys the object, releasing all held resources.
46
+ * After calling this, the object should be considered unusable.
47
+ */
48
+ destroy(): void;
49
+ }
50
+ /**
51
+ * Describes an object that can be unsubscribed from.
52
+ */
53
+ interface Unsubscribable {
54
+ unsubscribe(): void;
55
+ }
40
56
  /**
41
57
  * Defines the public, read-only contract for an event emitter.
42
58
  * It allows subscribing to an event but not emitting it.
@@ -48,7 +64,7 @@ type Subscribable<T extends any[]> = {
48
64
  * Subscribes a listener to this event.
49
65
  * @returns A function to unsubscribe the listener.
50
66
  */
51
- subscribe(listener: (...args: T) => void): () => void;
67
+ subscribe(listener: (...args: T) => void): Unsubscribable;
52
68
  unsubscribe(listener: (...args: T) => void): boolean;
53
69
  clear(): void;
54
70
  };
@@ -161,6 +177,7 @@ declare function throwIfEmpty<T>(value: T, exceptionMessage: string): asserts va
161
177
  */
162
178
  declare function throwError(message: string): never;
163
179
 
180
+ /** todo: rename to 'utils config' */
164
181
  interface AxiEngineConfig {
165
182
  pathSeparator: string;
166
183
  }
@@ -225,12 +242,44 @@ interface DataSink {
225
242
  * @param path The path to the value to be deleted.
226
243
  */
227
244
  delete(path: PathType): void;
245
+ /**
246
+ * Deletes all values
247
+ */
248
+ clear(): void;
228
249
  }
229
250
  /**
230
251
  * A full CRUD contract for systems that provide complete data management.
231
252
  * Combines both reading and writing capabilities.
232
253
  */
233
- interface DataStorage extends DataSource, DataSink {
254
+ interface DataStorage extends DataSource, DataSink, Destroyable {
255
+ }
256
+
257
+ /**
258
+ * Represents a disposable resource, such as the execution of an Observable or an Event Listener.
259
+ * Allows grouping multiple teardown logic into a single unit (Composite Subscription).
260
+ */
261
+ declare class Subscription implements Unsubscribable {
262
+ private _closed;
263
+ private _teardowns;
264
+ /**
265
+ * Indicates whether this subscription has already been unsubscribed.
266
+ */
267
+ get closed(): boolean;
268
+ /**
269
+ * @param teardown Optional initial teardown logic to execute when unsubscribed.
270
+ */
271
+ constructor(teardown?: () => void);
272
+ /**
273
+ * Adds a teardown logic to this subscription.
274
+ * If the subscription is already closed, the teardown is executed immediately.
275
+ * @param teardown A function or another Unsubscribable object to be managed.
276
+ */
277
+ add(teardown: Unsubscribable | (() => void)): void;
278
+ /**
279
+ * Disposes the resources held by the subscription.
280
+ * Executes all attached teardown logic and clears the list.
281
+ */
282
+ unsubscribe(): void;
234
283
  }
235
284
 
236
285
  /**
@@ -246,9 +295,15 @@ declare class Emitter<T extends any[]> implements Subscribable<T> {
246
295
  get listenerCount(): number;
247
296
  /**
248
297
  * Subscribes a listener to this event.
249
- * @returns A function to unsubscribe the listener.
298
+ * @returns A Subscription object to manage the unsubscription.
299
+ */
300
+ subscribe(listener: (...args: T) => void): Subscription;
301
+ /**
302
+ * Subscribes a listener that triggers only once and then automatically unsubscribes.
303
+ * @param listener The callback function to execute once.
304
+ * @returns A Subscription object (can be used to cancel before the event fires).
250
305
  */
251
- subscribe(listener: (...args: T) => void): () => void;
306
+ once(listener: (...args: T) => void): Subscription;
252
307
  /**
253
308
  * Manually unsubscribe by listener
254
309
  * @returns returns true if an listener has been removed, or false if the listener does not exist.
@@ -263,19 +318,32 @@ declare class Emitter<T extends any[]> implements Subscribable<T> {
263
318
  */
264
319
  clear(): void;
265
320
  }
321
+
266
322
  /**
267
323
  * An Emitter that stores the last emitted value.
268
324
  * New subscribers immediately receive the last value upon subscription.
269
325
  */
270
326
  declare class StateEmitter<T extends any[]> extends Emitter<T> {
271
327
  private _lastValue;
328
+ /**
329
+ * @param initialValue Optional initial value to set.
330
+ */
272
331
  constructor(initialValue?: T);
273
332
  /**
274
333
  * Gets the current value synchronously without subscribing.
275
334
  */
276
335
  get value(): T | undefined;
336
+ /**
337
+ * Updates the state and notifies all listeners.
338
+ * @param args The new value(s).
339
+ */
277
340
  emit(...args: T): void;
278
- subscribe(listener: (...args: T) => void): () => void;
341
+ /**
342
+ * Subscribes to the event. If a value exists, the listener is called immediately.
343
+ * @param listener The callback function.
344
+ * @returns A Subscription object.
345
+ */
346
+ subscribe(listener: (...args: T) => void): Subscription;
279
347
  clear(): void;
280
348
  }
281
349
 
@@ -362,15 +430,18 @@ declare function clampNumber(val: number, min?: number | null, max?: number | nu
362
430
  * Calculates a percentage of a given value.
363
431
  * @param val The base value.
364
432
  * @param percents The percentage to get.
433
+ * @param precision Optional number of decimal places to round the result to.
365
434
  * @returns The calculated percentage of the value.
366
- * @example getPercentOf(200, 10); // returns 20
435
+ * @example getPercentOf(200, 12.5); // returns 25
436
+ * @example getPercentOf(100, 33.333, 2); // returns 33.33
367
437
  */
368
- declare function getPercentOf(val: number, percents: number): number;
438
+ declare function getPercentOf(val: number, percents: number, precision?: number): number;
369
439
 
370
440
  /**
371
441
  * Returns the first key of an object.
372
442
  * @param obj The object from which to get the key.
373
443
  * @returns The first key of the object as a string.
444
+ * @throws {Error} If the argument is not a valid object.
374
445
  */
375
446
  declare function firstKeyOf(obj: any): string;
376
447
 
@@ -395,7 +466,7 @@ declare function randInt(min: number, max: number): number;
395
466
  * Generates a unique identifier using uuidv4.
396
467
  * @returns A unique string ID.
397
468
  */
398
- declare function randId(): string;
469
+ declare function uid(): string;
399
470
 
400
471
  /**
401
472
  * A generic, type-safe wrapper around a Map for managing collections of items by key.
@@ -439,4 +510,4 @@ declare class Registry<K extends PropertyKey, V> {
439
510
  clear(): void;
440
511
  }
441
512
 
442
- export { type AxiEngineConfig, type Constructor, type DataSink, type DataSource, type DataStorage, Emitter, type PathType, Registry, type ScalarType, StateEmitter, type Subscribable, areArraysEqual, axiSettings, clampNumber, configure, ensurePathArray, ensurePathString, firstKeyOf, genArray, getPercentOf, getRandomElement, haveSameElements, isBoolean, isFunction, isNull, isNullOrUndefined, isNumber, isObject, isPercentageString, isPromise, isScalar, isSequentialStart, isString, isUndefined, last, randId, randInt, shuffleArray, throwError, throwIf, throwIfEmpty, unique };
513
+ export { type AxiEngineConfig, type Constructor, type DataSink, type DataSource, type DataStorage, type Destroyable, Emitter, type PathType, Registry, type ScalarType, StateEmitter, type Subscribable, Subscription, type Unsubscribable, areArraysEqual, axiSettings, clampNumber, configure, ensurePathArray, ensurePathString, firstKeyOf, genArray, getPercentOf, getRandomElement, haveSameElements, isBoolean, isFunction, isNull, isNullOrUndefined, isNumber, isObject, isPercentageString, isPromise, isScalar, isSequentialStart, isString, isUndefined, last, randInt, shuffleArray, throwError, throwIf, throwIfEmpty, uid, unique };
package/dist/index.js CHANGED
@@ -23,6 +23,7 @@ __export(index_exports, {
23
23
  Emitter: () => Emitter,
24
24
  Registry: () => Registry,
25
25
  StateEmitter: () => StateEmitter,
26
+ Subscription: () => Subscription,
26
27
  areArraysEqual: () => areArraysEqual,
27
28
  axiSettings: () => axiSettings,
28
29
  clampNumber: () => clampNumber,
@@ -47,12 +48,12 @@ __export(index_exports, {
47
48
  isString: () => isString,
48
49
  isUndefined: () => isUndefined,
49
50
  last: () => last,
50
- randId: () => randId,
51
51
  randInt: () => randInt,
52
52
  shuffleArray: () => shuffleArray,
53
53
  throwError: () => throwError,
54
54
  throwIf: () => throwIf,
55
55
  throwIfEmpty: () => throwIfEmpty,
56
+ uid: () => uid,
56
57
  unique: () => unique
57
58
  });
58
59
  module.exports = __toCommonJS(index_exports);
@@ -136,7 +137,7 @@ function isPromise(value) {
136
137
  return value != null && typeof value.then === "function";
137
138
  }
138
139
  function isPercentageString(val) {
139
- return typeof val === "string" && val.endsWith("%");
140
+ return isString(val) && val.endsWith("%");
140
141
  }
141
142
 
142
143
  // src/assertion.ts
@@ -146,10 +147,10 @@ function throwIf(condition, exceptionMessage) {
146
147
  }
147
148
  }
148
149
  function throwIfEmpty(value, exceptionMessage) {
149
- const isArrayAndEmpty = Array.isArray(value) && value.length === 0;
150
- if (isNullOrUndefined(value) || isArrayAndEmpty) {
151
- throw new Error(exceptionMessage);
152
- }
150
+ throwIf(
151
+ isNullOrUndefined(value) || Array.isArray(value) && !value.length,
152
+ exceptionMessage
153
+ );
153
154
  }
154
155
  function throwError(message) {
155
156
  throw new Error(message);
@@ -157,14 +158,56 @@ function throwError(message) {
157
158
 
158
159
  // src/config.ts
159
160
  var defaultConfig = {
160
- pathSeparator: "/"
161
+ pathSeparator: "."
161
162
  };
162
163
  var axiSettings = { ...defaultConfig };
163
164
  function configure(newConfig) {
164
165
  Object.assign(axiSettings, newConfig);
165
166
  }
166
167
 
167
- // src/emitter.ts
168
+ // src/subscription.ts
169
+ var Subscription = class {
170
+ _closed = false;
171
+ _teardowns = [];
172
+ /**
173
+ * Indicates whether this subscription has already been unsubscribed.
174
+ */
175
+ get closed() {
176
+ return this._closed;
177
+ }
178
+ /**
179
+ * @param teardown Optional initial teardown logic to execute when unsubscribed.
180
+ */
181
+ constructor(teardown) {
182
+ if (teardown) {
183
+ this._teardowns.push(teardown);
184
+ }
185
+ }
186
+ /**
187
+ * Adds a teardown logic to this subscription.
188
+ * If the subscription is already closed, the teardown is executed immediately.
189
+ * @param teardown A function or another Unsubscribable object to be managed.
190
+ */
191
+ add(teardown) {
192
+ if (this._closed) {
193
+ isFunction(teardown) ? teardown() : teardown.unsubscribe();
194
+ return;
195
+ }
196
+ this._teardowns.push(isFunction(teardown) ? teardown : () => teardown.unsubscribe());
197
+ }
198
+ /**
199
+ * Disposes the resources held by the subscription.
200
+ * Executes all attached teardown logic and clears the list.
201
+ */
202
+ unsubscribe() {
203
+ if (this._closed) return;
204
+ this._closed = true;
205
+ this._teardowns.forEach((fn) => fn());
206
+ this._teardowns = [];
207
+ }
208
+ };
209
+
210
+ // src/emitters/emitter.ts
168
211
  var Emitter = class {
169
212
  listeners = /* @__PURE__ */ new Set();
170
213
  /**
@@ -175,11 +218,23 @@ var Emitter = class {
175
218
  }
176
219
  /**
177
220
  * Subscribes a listener to this event.
178
- * @returns A function to unsubscribe the listener.
221
+ * @returns A Subscription object to manage the unsubscription.
179
222
  */
180
223
  subscribe(listener) {
181
224
  this.listeners.add(listener);
182
- return () => this.listeners.delete(listener);
225
+ return new Subscription(() => this.unsubscribe(listener));
226
+ }
227
+ /**
228
+ * Subscribes a listener that triggers only once and then automatically unsubscribes.
229
+ * @param listener The callback function to execute once.
230
+ * @returns A Subscription object (can be used to cancel before the event fires).
231
+ */
232
+ once(listener) {
233
+ const wrapper = (...args) => {
234
+ this.unsubscribe(wrapper);
235
+ listener(...args);
236
+ };
237
+ return this.subscribe(wrapper);
183
238
  }
184
239
  /**
185
240
  * Manually unsubscribe by listener
@@ -201,8 +256,13 @@ var Emitter = class {
201
256
  this.listeners.clear();
202
257
  }
203
258
  };
259
+
260
+ // src/emitters/state-emitter.ts
204
261
  var StateEmitter = class extends Emitter {
205
262
  _lastValue;
263
+ /**
264
+ * @param initialValue Optional initial value to set.
265
+ */
206
266
  constructor(initialValue) {
207
267
  super();
208
268
  this._lastValue = initialValue ?? void 0;
@@ -213,10 +273,19 @@ var StateEmitter = class extends Emitter {
213
273
  get value() {
214
274
  return this._lastValue;
215
275
  }
276
+ /**
277
+ * Updates the state and notifies all listeners.
278
+ * @param args The new value(s).
279
+ */
216
280
  emit(...args) {
217
281
  this._lastValue = args;
218
282
  super.emit(...args);
219
283
  }
284
+ /**
285
+ * Subscribes to the event. If a value exists, the listener is called immediately.
286
+ * @param listener The callback function.
287
+ * @returns A Subscription object.
288
+ */
220
289
  subscribe(listener) {
221
290
  const unsubscribe = super.subscribe(listener);
222
291
  if (!isUndefined(this._lastValue)) {
@@ -236,12 +305,17 @@ function clampNumber(val, min, max) {
236
305
  if (!isNullOrUndefined(max)) val = Math.min(val, max);
237
306
  return val;
238
307
  }
239
- function getPercentOf(val, percents) {
240
- return percents / 100 * val;
308
+ function getPercentOf(val, percents, precision) {
309
+ const result = percents / 100 * val;
310
+ if (!isUndefined(precision)) {
311
+ return Number(result.toFixed(precision));
312
+ }
313
+ return result;
241
314
  }
242
315
 
243
316
  // src/misc.ts
244
317
  function firstKeyOf(obj) {
318
+ throwIf(!isObject(obj), `firstKeyOf: Expected an object, got ${typeof obj}`);
245
319
  return Object.keys(obj)[0];
246
320
  }
247
321
 
@@ -260,7 +334,7 @@ function randInt(min, max) {
260
334
  max = Math.floor(max);
261
335
  return Math.floor(Math.random() * (max - min) + min);
262
336
  }
263
- function randId() {
337
+ function uid() {
264
338
  return (0, import_uuid.v4)();
265
339
  }
266
340
 
@@ -318,6 +392,7 @@ var Registry = class {
318
392
  Emitter,
319
393
  Registry,
320
394
  StateEmitter,
395
+ Subscription,
321
396
  areArraysEqual,
322
397
  axiSettings,
323
398
  clampNumber,
@@ -342,11 +417,11 @@ var Registry = class {
342
417
  isString,
343
418
  isUndefined,
344
419
  last,
345
- randId,
346
420
  randInt,
347
421
  shuffleArray,
348
422
  throwError,
349
423
  throwIf,
350
424
  throwIfEmpty,
425
+ uid,
351
426
  unique
352
427
  });
package/dist/index.mjs CHANGED
@@ -77,7 +77,7 @@ function isPromise(value) {
77
77
  return value != null && typeof value.then === "function";
78
78
  }
79
79
  function isPercentageString(val) {
80
- return typeof val === "string" && val.endsWith("%");
80
+ return isString(val) && val.endsWith("%");
81
81
  }
82
82
 
83
83
  // src/assertion.ts
@@ -87,10 +87,10 @@ function throwIf(condition, exceptionMessage) {
87
87
  }
88
88
  }
89
89
  function throwIfEmpty(value, exceptionMessage) {
90
- const isArrayAndEmpty = Array.isArray(value) && value.length === 0;
91
- if (isNullOrUndefined(value) || isArrayAndEmpty) {
92
- throw new Error(exceptionMessage);
93
- }
90
+ throwIf(
91
+ isNullOrUndefined(value) || Array.isArray(value) && !value.length,
92
+ exceptionMessage
93
+ );
94
94
  }
95
95
  function throwError(message) {
96
96
  throw new Error(message);
@@ -98,14 +98,56 @@ function throwError(message) {
98
98
 
99
99
  // src/config.ts
100
100
  var defaultConfig = {
101
- pathSeparator: "/"
101
+ pathSeparator: "."
102
102
  };
103
103
  var axiSettings = { ...defaultConfig };
104
104
  function configure(newConfig) {
105
105
  Object.assign(axiSettings, newConfig);
106
106
  }
107
107
 
108
- // src/emitter.ts
108
+ // src/subscription.ts
109
+ var Subscription = class {
110
+ _closed = false;
111
+ _teardowns = [];
112
+ /**
113
+ * Indicates whether this subscription has already been unsubscribed.
114
+ */
115
+ get closed() {
116
+ return this._closed;
117
+ }
118
+ /**
119
+ * @param teardown Optional initial teardown logic to execute when unsubscribed.
120
+ */
121
+ constructor(teardown) {
122
+ if (teardown) {
123
+ this._teardowns.push(teardown);
124
+ }
125
+ }
126
+ /**
127
+ * Adds a teardown logic to this subscription.
128
+ * If the subscription is already closed, the teardown is executed immediately.
129
+ * @param teardown A function or another Unsubscribable object to be managed.
130
+ */
131
+ add(teardown) {
132
+ if (this._closed) {
133
+ isFunction(teardown) ? teardown() : teardown.unsubscribe();
134
+ return;
135
+ }
136
+ this._teardowns.push(isFunction(teardown) ? teardown : () => teardown.unsubscribe());
137
+ }
138
+ /**
139
+ * Disposes the resources held by the subscription.
140
+ * Executes all attached teardown logic and clears the list.
141
+ */
142
+ unsubscribe() {
143
+ if (this._closed) return;
144
+ this._closed = true;
145
+ this._teardowns.forEach((fn) => fn());
146
+ this._teardowns = [];
147
+ }
148
+ };
149
+
150
+ // src/emitters/emitter.ts
109
151
  var Emitter = class {
110
152
  listeners = /* @__PURE__ */ new Set();
111
153
  /**
@@ -116,11 +158,23 @@ var Emitter = class {
116
158
  }
117
159
  /**
118
160
  * Subscribes a listener to this event.
119
- * @returns A function to unsubscribe the listener.
161
+ * @returns A Subscription object to manage the unsubscription.
120
162
  */
121
163
  subscribe(listener) {
122
164
  this.listeners.add(listener);
123
- return () => this.listeners.delete(listener);
165
+ return new Subscription(() => this.unsubscribe(listener));
166
+ }
167
+ /**
168
+ * Subscribes a listener that triggers only once and then automatically unsubscribes.
169
+ * @param listener The callback function to execute once.
170
+ * @returns A Subscription object (can be used to cancel before the event fires).
171
+ */
172
+ once(listener) {
173
+ const wrapper = (...args) => {
174
+ this.unsubscribe(wrapper);
175
+ listener(...args);
176
+ };
177
+ return this.subscribe(wrapper);
124
178
  }
125
179
  /**
126
180
  * Manually unsubscribe by listener
@@ -142,8 +196,13 @@ var Emitter = class {
142
196
  this.listeners.clear();
143
197
  }
144
198
  };
199
+
200
+ // src/emitters/state-emitter.ts
145
201
  var StateEmitter = class extends Emitter {
146
202
  _lastValue;
203
+ /**
204
+ * @param initialValue Optional initial value to set.
205
+ */
147
206
  constructor(initialValue) {
148
207
  super();
149
208
  this._lastValue = initialValue ?? void 0;
@@ -154,10 +213,19 @@ var StateEmitter = class extends Emitter {
154
213
  get value() {
155
214
  return this._lastValue;
156
215
  }
216
+ /**
217
+ * Updates the state and notifies all listeners.
218
+ * @param args The new value(s).
219
+ */
157
220
  emit(...args) {
158
221
  this._lastValue = args;
159
222
  super.emit(...args);
160
223
  }
224
+ /**
225
+ * Subscribes to the event. If a value exists, the listener is called immediately.
226
+ * @param listener The callback function.
227
+ * @returns A Subscription object.
228
+ */
161
229
  subscribe(listener) {
162
230
  const unsubscribe = super.subscribe(listener);
163
231
  if (!isUndefined(this._lastValue)) {
@@ -177,12 +245,17 @@ function clampNumber(val, min, max) {
177
245
  if (!isNullOrUndefined(max)) val = Math.min(val, max);
178
246
  return val;
179
247
  }
180
- function getPercentOf(val, percents) {
181
- return percents / 100 * val;
248
+ function getPercentOf(val, percents, precision) {
249
+ const result = percents / 100 * val;
250
+ if (!isUndefined(precision)) {
251
+ return Number(result.toFixed(precision));
252
+ }
253
+ return result;
182
254
  }
183
255
 
184
256
  // src/misc.ts
185
257
  function firstKeyOf(obj) {
258
+ throwIf(!isObject(obj), `firstKeyOf: Expected an object, got ${typeof obj}`);
186
259
  return Object.keys(obj)[0];
187
260
  }
188
261
 
@@ -201,7 +274,7 @@ function randInt(min, max) {
201
274
  max = Math.floor(max);
202
275
  return Math.floor(Math.random() * (max - min) + min);
203
276
  }
204
- function randId() {
277
+ function uid() {
205
278
  return uuidv4();
206
279
  }
207
280
 
@@ -258,6 +331,7 @@ export {
258
331
  Emitter,
259
332
  Registry,
260
333
  StateEmitter,
334
+ Subscription,
261
335
  areArraysEqual,
262
336
  axiSettings,
263
337
  clampNumber,
@@ -282,11 +356,11 @@ export {
282
356
  isString,
283
357
  isUndefined,
284
358
  last,
285
- randId,
286
359
  randInt,
287
360
  shuffleArray,
288
361
  throwError,
289
362
  throwIf,
290
363
  throwIfEmpty,
364
+ uid,
291
365
  unique
292
366
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axi-engine/utils",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "Core utility library for Axi Engine, providing common functions for arrays, math, type guards, and more.",
5
5
  "license": "MIT",
6
6
  "repository": {