@monterosa/sdk-util 0.19.0-rc.6 → 2.0.0-rc.2

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.
@@ -8,6 +8,8 @@
8
8
  * More details on the license can be found at https://www.monterosa.co/sdk/license
9
9
  */
10
10
  /**
11
+ * @internal
12
+ *
11
13
  * FNV-1a 32-bit hash function
12
14
  * Produces a fast, non-cryptographic checksum for strings
13
15
  *
package/dist/delay.d.ts CHANGED
@@ -8,5 +8,11 @@
8
8
  */
9
9
  /**
10
10
  * @internal
11
+ *
12
+ * Delays the execution of the code for a given number of milliseconds.
13
+ *
14
+ * @param timeout - The timeout in milliseconds.
15
+ *
16
+ * @returns A promise that resolves after the timeout.
11
17
  */
12
18
  export declare const delay: (timeout: number) => Promise<void>;
package/dist/emitter.d.ts CHANGED
@@ -7,13 +7,43 @@
7
7
  * More details on the license can be found at https://www.monterosa.co/sdk/license
8
8
  */
9
9
  type Handler = (...args: any[]) => void;
10
+ /**
11
+ * @internal
12
+ *
13
+ * Event emitter. Provides subscribe/unsubscribe pattern used throughout
14
+ * the SDK for observable state changes.
15
+ */
10
16
  export declare class Emitter {
11
- private readonly emitter;
12
- private readonly handlers;
13
- constructor();
17
+ private readonly listeners;
18
+ /**
19
+ * Subscribes to an event.
20
+ *
21
+ * @param event - The event name
22
+ * @param handler - The callback function
23
+ * @returns A function that unsubscribes the handler when called
24
+ */
14
25
  on(event: string, handler: Handler): Unsubscribe;
26
+ /**
27
+ * Unsubscribes from an event.
28
+ *
29
+ * @param event - The event name
30
+ * @param handler - The handler to remove. If omitted, all handlers for the event are removed.
31
+ */
15
32
  off(event: string, handler?: Handler): void;
33
+ /**
34
+ * Emits an event with optional arguments.
35
+ *
36
+ * @param event - The event name
37
+ * @param args - Arguments to pass to handlers
38
+ */
16
39
  emit(event: string, ...args: any[]): void;
40
+ /**
41
+ * Subscribes to an event and automatically unsubscribes after the first call.
42
+ *
43
+ * @param event - The event name
44
+ * @param handler - The callback function
45
+ * @returns A function that unsubscribes the handler when called
46
+ */
17
47
  once(event: string, handler: Handler): Unsubscribe;
18
48
  }
19
49
  /**
package/dist/error.d.ts CHANGED
@@ -8,8 +8,8 @@
8
8
  */
9
9
  /**
10
10
  * MonterosaError extends the standard JavaScript `Error` object. It has
11
- * an error code so that user can identify the error. Also it has it's own
12
- * specific `name` "MonterosaError"
11
+ * an error code so that users can identify the error. It also has its
12
+ * own specific `name` "MonterosaError".
13
13
  */
14
14
  export declare class MonterosaError extends Error {
15
15
  /**
@@ -33,4 +33,8 @@ type ErrorMap<ErrorCode extends string> = {
33
33
  * @internal
34
34
  */
35
35
  export declare function createError<ErrorCode extends string>(code: ErrorCode, messages: ErrorMap<ErrorCode>, ...params: any[]): MonterosaError;
36
+ /**
37
+ * @internal
38
+ */
39
+ export declare function getErrorMessage(err: unknown): string;
36
40
  export {};
@@ -7,4 +7,7 @@
7
7
  *
8
8
  * More details on the license can be found at https://www.monterosa.co/sdk/license
9
9
  */
10
+ /**
11
+ * @internal
12
+ */
10
13
  export declare function fromEntries(entries: [string | number | symbol, any][]): Record<string | number | symbol, any>;
package/dist/index.d.ts CHANGED
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Shared utilities including emitters, error handling, and storage helpers.
3
+ *
4
+ * @packageDocumentation
5
+ */
1
6
  /**
2
7
  * @license
3
8
  * @monterosa/sdk-util
@@ -6,11 +11,6 @@
6
11
  *
7
12
  * More details on the license can be found at https://www.monterosa.co/sdk/license
8
13
  */
9
- /**
10
- * Monterosa SDK / Util
11
- *
12
- * @packageDocumentation
13
- */
14
14
  export * from './emitter';
15
15
  export * from './delay';
16
16
  export * from './memoize-promise';
@@ -21,4 +21,7 @@ export * from './throttle';
21
21
  export * from './time';
22
22
  export * from './fromentries';
23
23
  export * from './calculate-percentage';
24
+ export * from './task-queue';
25
+ export * from './with-retry-async';
24
26
  export * from './checksum';
27
+ export * from './uuid';
@@ -1,5 +1,3 @@
1
- import mitt from 'mitt';
2
-
3
1
  /**
4
2
  * @license
5
3
  * @monterosa/sdk-util
@@ -8,41 +6,67 @@ import mitt from 'mitt';
8
6
  *
9
7
  * More details on the license can be found at https://www.monterosa.co/sdk/license
10
8
  */
9
+ /**
10
+ * @internal
11
+ *
12
+ * Event emitter. Provides subscribe/unsubscribe pattern used throughout
13
+ * the SDK for observable state changes.
14
+ */
11
15
  class Emitter {
12
16
  constructor() {
13
- this.handlers = new Map();
14
- this.emitter = mitt();
17
+ this.listeners = new Map();
15
18
  }
19
+ /**
20
+ * Subscribes to an event.
21
+ *
22
+ * @param event - The event name
23
+ * @param handler - The callback function
24
+ * @returns A function that unsubscribes the handler when called
25
+ */
16
26
  on(event, handler) {
17
- const mittHandler = (args) => {
18
- handler(...(Array.isArray(args) ? args : [args]));
19
- };
20
- this.handlers.set(handler, mittHandler);
21
- this.emitter.on(event, mittHandler);
27
+ if (!this.listeners.has(event)) {
28
+ this.listeners.set(event, new Set());
29
+ }
30
+ this.listeners.get(event).add(handler);
22
31
  return () => this.off(event, handler);
23
32
  }
33
+ /**
34
+ * Unsubscribes from an event.
35
+ *
36
+ * @param event - The event name
37
+ * @param handler - The handler to remove. If omitted, all handlers for the event are removed.
38
+ */
24
39
  off(event, handler) {
40
+ var _a;
25
41
  if (handler === undefined) {
26
- this.emitter.off(event);
42
+ this.listeners.delete(event);
27
43
  return;
28
44
  }
29
- const mittHandler = this.handlers.get(handler);
30
- if (mittHandler) {
31
- this.emitter.off(event, mittHandler);
32
- this.handlers.delete(handler);
33
- }
45
+ (_a = this.listeners.get(event)) === null || _a === void 0 ? void 0 : _a.delete(handler);
34
46
  }
47
+ /**
48
+ * Emits an event with optional arguments.
49
+ *
50
+ * @param event - The event name
51
+ * @param args - Arguments to pass to handlers
52
+ */
35
53
  emit(event, ...args) {
36
- this.emitter.emit(event, args);
54
+ var _a;
55
+ (_a = this.listeners.get(event)) === null || _a === void 0 ? void 0 : _a.forEach((handler) => handler(...args));
37
56
  }
57
+ /**
58
+ * Subscribes to an event and automatically unsubscribes after the first call.
59
+ *
60
+ * @param event - The event name
61
+ * @param handler - The callback function
62
+ * @returns A function that unsubscribes the handler when called
63
+ */
38
64
  once(event, handler) {
39
- const mittHandler = (args) => {
40
- handler(...(Array.isArray(args) ? args : [args]));
41
- this.off(event, handler);
65
+ const wrapper = (...args) => {
66
+ handler(...args);
67
+ this.off(event, wrapper);
42
68
  };
43
- this.handlers.set(handler, mittHandler);
44
- this.emitter.on(event, mittHandler);
45
- return () => this.off(event, handler);
69
+ return this.on(event, wrapper);
46
70
  }
47
71
  }
48
72
  /**
@@ -63,6 +87,12 @@ const subscribe = (emitter, event, callback) => {
63
87
  */
64
88
  /**
65
89
  * @internal
90
+ *
91
+ * Delays the execution of the code for a given number of milliseconds.
92
+ *
93
+ * @param timeout - The timeout in milliseconds.
94
+ *
95
+ * @returns A promise that resolves after the timeout.
66
96
  */
67
97
  const delay = async (timeout) => {
68
98
  await new Promise((resolve) => setTimeout(resolve, timeout));
@@ -213,8 +243,8 @@ function checkAvailability() {
213
243
  /* eslint max-classes-per-file: ["error", 2] */
214
244
  /**
215
245
  * MonterosaError extends the standard JavaScript `Error` object. It has
216
- * an error code so that user can identify the error. Also it has it's own
217
- * specific `name` "MonterosaError"
246
+ * an error code so that users can identify the error. It also has its
247
+ * own specific `name` "MonterosaError".
218
248
  */
219
249
  class MonterosaError extends Error {
220
250
  /**
@@ -239,6 +269,23 @@ class MonterosaError extends Error {
239
269
  function createError(code, messages, ...params) {
240
270
  const message = messages[code](...params);
241
271
  return new MonterosaError(code, message);
272
+ }
273
+ /**
274
+ * @internal
275
+ */
276
+ function getErrorMessage(err) {
277
+ if (err instanceof Error) {
278
+ return err.message;
279
+ }
280
+ if (typeof err === 'string') {
281
+ return err;
282
+ }
283
+ try {
284
+ return JSON.stringify(err);
285
+ }
286
+ catch (_a) {
287
+ return 'Unknown error';
288
+ }
242
289
  }
243
290
 
244
291
  /**
@@ -686,6 +733,8 @@ function calculateNextTickDelay() {
686
733
  return (expectedNextTick - serverTimestamp) * 1000;
687
734
  }
688
735
  /**
736
+ * @internal
737
+ *
689
738
  * Main function that maintains current timestamp
690
739
  */
691
740
  function tick() {
@@ -695,6 +744,14 @@ function tick() {
695
744
  serverTimestamp += timeSinceLastTick;
696
745
  lastTickTimestamp = currentTimestamp;
697
746
  tickTimeoutId = setTimeout(tick, calculateNextTickDelay());
747
+ // In Node.js, a pending setTimeout keeps the process alive. This causes
748
+ // Jest worker processes to hang after tests complete, because tick()
749
+ // reschedules itself indefinitely. Calling unref() tells Node.js not to
750
+ // keep the process alive solely for this timer. In browsers, unref()
751
+ // does not exist and is not needed — tabs don't have this problem.
752
+ if (typeof tickTimeoutId === 'object' && 'unref' in tickTimeoutId) {
753
+ tickTimeoutId.unref();
754
+ }
698
755
  emitter.emit('tick', serverTimestamp);
699
756
  }
700
757
  /**
@@ -743,6 +800,9 @@ tick();
743
800
  *
744
801
  * More details on the license can be found at https://www.monterosa.co/sdk/license
745
802
  */
803
+ /**
804
+ * @internal
805
+ */
746
806
  function fromEntries(entries) {
747
807
  return entries.reduce((obj, [key, value]) => {
748
808
  obj[key] = value;
@@ -805,6 +865,179 @@ const calculatePercentage = (values) => {
805
865
  return results.map((item) => item.percentage);
806
866
  };
807
867
 
868
+ /**
869
+ * @license
870
+ * @monterosa/sdk-identify-kit
871
+ *
872
+ * Copyright © 2025 Monterosa Productions Limited. All rights reserved.
873
+ *
874
+ * More details on the license can be found at https://www.monterosa.co/sdk/license
875
+ */
876
+ /**
877
+ * @internal
878
+ *
879
+ * TaskQueue ensures that async tasks run sequentially (FIFO).
880
+ *
881
+ * Behavior:
882
+ * - Tasks run one at a time in the order they were enqueued.
883
+ * - If a task fails (throws/rejects), the queue is stopped
884
+ * and all pending tasks are cleared.
885
+ */
886
+ class TaskQueue {
887
+ constructor() {
888
+ this.current = null;
889
+ this.queue = [];
890
+ this.paused = false;
891
+ }
892
+ enqueue(fn) {
893
+ this.queue.push(fn);
894
+ this.process();
895
+ }
896
+ enqueueFront(fn) {
897
+ this.queue.unshift(fn);
898
+ this.process();
899
+ }
900
+ pause() {
901
+ this.paused = true;
902
+ }
903
+ resume() {
904
+ this.paused = false;
905
+ this.process();
906
+ }
907
+ process() {
908
+ if (this.current || this.paused) {
909
+ return;
910
+ }
911
+ const next = this.queue.shift();
912
+ if (!next) {
913
+ return;
914
+ }
915
+ this.current = next()
916
+ .then(() => {
917
+ this.current = null;
918
+ this.process();
919
+ })
920
+ .catch(() => {
921
+ this.clear();
922
+ });
923
+ }
924
+ get length() {
925
+ return this.queue.length;
926
+ }
927
+ isEmpty() {
928
+ return this.queue.length === 0;
929
+ }
930
+ hasActiveTask() {
931
+ return this.current !== null;
932
+ }
933
+ clear() {
934
+ this.queue = [];
935
+ this.current = null;
936
+ }
937
+ }
938
+
939
+ /**
940
+ * @license
941
+ * @monterosa/sdk-util
942
+ *
943
+ * Copyright © 2025 Monterosa Productions Limited. All rights reserved.
944
+ *
945
+ * More details on the license can be found at https://www.monterosa.co/sdk/license
946
+ */
947
+ /**
948
+ * Calculates the delay for a retry attempt based on the backoff strategy.
949
+ *
950
+ * @param backoffStrategy - The backoff strategy.
951
+ * @param attemptNumber - The current attempt number (0-indexed).
952
+ * @param baseDelay - The base delay in milliseconds.
953
+ * @param jitter - The maximum jitter to add in milliseconds.
954
+ * @param maxDelay - The maximum delay in milliseconds.
955
+ *
956
+ * @returns The calculated delay in milliseconds.
957
+ */
958
+ function calculateRetryDelay(backoffStrategy, attemptNumber, baseDelay, jitter, maxDelay) {
959
+ let delayMs;
960
+ switch (backoffStrategy) {
961
+ case 'fixed':
962
+ delayMs = baseDelay;
963
+ break;
964
+ case 'incremental':
965
+ delayMs = baseDelay * (attemptNumber + 1);
966
+ break;
967
+ case 'exponential':
968
+ default:
969
+ delayMs = baseDelay * 2 ** attemptNumber;
970
+ }
971
+ // Add jitter (random value between 0 and jitter)
972
+ delayMs += Math.floor(Math.random() * jitter);
973
+ // Cap at maxDelay
974
+ return Math.min(delayMs, maxDelay);
975
+ }
976
+ /**
977
+ * @internal
978
+ *
979
+ * Default configuration values for retry logic.
980
+ */
981
+ const DEFAULT_RETRY_CONFIG = {
982
+ backoffStrategy: 'exponential',
983
+ attempts: 3,
984
+ baseDelay: 500,
985
+ jitter: 500,
986
+ maxDelay: 10000,
987
+ };
988
+ /**
989
+ * @internal
990
+ *
991
+ * Wraps an async function with retry logic and configurable backoff strategies.
992
+ *
993
+ * @template TArgs - The argument types of the wrapped function.
994
+ * @template TResult - The return type of the wrapped function.
995
+ *
996
+ * @param fn - The async function to wrap.
997
+ * @param config - Configuration object for retry behaviour.
998
+ * - backoffStrategy: BackoffStrategy (default: 'exponential')
999
+ * - attempts: number of retry attempts (default: 3)
1000
+ * - baseDelay: base delay in ms (default: 500)
1001
+ * - jitter: max jitter in ms (default: 500)
1002
+ * - maxDelay: max delay in ms (default: 10_000)
1003
+ *
1004
+ * @returns A new function that retries the given async function up to the
1005
+ * specified number of attempts with the configured backoff strategy.
1006
+ *
1007
+ * @throws The last encountered error if all retry attempts fail.
1008
+ *
1009
+ * @example
1010
+ * const fetchDataWithRetry = withRetryAsync(fetchData, {
1011
+ * backoffStrategy: 'exponential',
1012
+ * attempts: 3,
1013
+ * baseDelay: 500
1014
+ * });
1015
+ *
1016
+ * // Will retry up to 3 times with exponential backoff
1017
+ * fetchDataWithRetry("https://api.example.com")
1018
+ * .then(data => console.log(data))
1019
+ * .catch(err => console.error("Failed after retries:", err));
1020
+ */
1021
+ function withRetryAsync(fn, config = {}) {
1022
+ const { backoffStrategy = DEFAULT_RETRY_CONFIG.backoffStrategy, attempts = DEFAULT_RETRY_CONFIG.attempts, baseDelay = DEFAULT_RETRY_CONFIG.baseDelay, jitter = DEFAULT_RETRY_CONFIG.jitter, maxDelay = DEFAULT_RETRY_CONFIG.maxDelay, } = config;
1023
+ return async (...args) => {
1024
+ let lastError;
1025
+ for (let i = 0; i < attempts; i++) {
1026
+ try {
1027
+ return await fn(...args);
1028
+ }
1029
+ catch (error) {
1030
+ lastError = error;
1031
+ if (i < attempts - 1) {
1032
+ const delayMs = calculateRetryDelay(backoffStrategy, i, baseDelay, jitter, maxDelay);
1033
+ await delay(delayMs);
1034
+ }
1035
+ }
1036
+ }
1037
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
1038
+ };
1039
+ }
1040
+
808
1041
  /**
809
1042
  * @license
810
1043
  * checksum.ts
@@ -846,6 +1079,8 @@ else {
846
1079
  TextEncoderRef = TextEncoder;
847
1080
  }
848
1081
  /**
1082
+ * @internal
1083
+ *
849
1084
  * FNV-1a 32-bit hash function
850
1085
  * Produces a fast, non-cryptographic checksum for strings
851
1086
  *
@@ -872,5 +1107,49 @@ function checksum(str) {
872
1107
  return hash.toString(16).padStart(8, '0');
873
1108
  }
874
1109
 
875
- export { Emitter, MonterosaError, calculatePercentage, checkAvailability, checksum, clear, createError, delay, fromEntries, getGlobal, getItem, getKey, memoizePromise, now, onTick, removeItem, setItem, setTimestamp, subscribe, throttle, tick };
876
- //# sourceMappingURL=index.esm.js.map
1110
+ /**
1111
+ * @license
1112
+ * @monterosa/sdk-util
1113
+ *
1114
+ * Copyright © 2026 Monterosa Productions Limited. All rights reserved.
1115
+ *
1116
+ * More details on the license can be found at https://www.monterosa.co/sdk/license
1117
+ */
1118
+ /* eslint-disable no-bitwise */
1119
+ const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
1120
+ /**
1121
+ * Generates a RFC 4122 version 4 UUID.
1122
+ *
1123
+ * Uses `crypto.randomUUID()` when available, falling back to
1124
+ * `crypto.getRandomValues()` for older browsers.
1125
+ *
1126
+ * @returns A UUID v4 string
1127
+ *
1128
+ * @internal
1129
+ */
1130
+ function generateUUID() {
1131
+ if (typeof crypto.randomUUID === 'function') {
1132
+ return crypto.randomUUID();
1133
+ }
1134
+ const bytes = crypto.getRandomValues(new Uint8Array(16));
1135
+ bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4
1136
+ bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 10
1137
+ const hex = Array.from(bytes)
1138
+ .map((b) => b.toString(16).padStart(2, '0'))
1139
+ .join('');
1140
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
1141
+ }
1142
+ /**
1143
+ * Validates whether a string is a valid UUID v4.
1144
+ *
1145
+ * @param value - The string to validate
1146
+ * @returns `true` if the string is a valid UUID v4
1147
+ *
1148
+ * @internal
1149
+ */
1150
+ function isUUID(value) {
1151
+ return UUID_V4_REGEX.test(value);
1152
+ }
1153
+
1154
+ export { DEFAULT_RETRY_CONFIG, Emitter, MonterosaError, TaskQueue, calculatePercentage, checkAvailability, checksum, clear, createError, delay, fromEntries, generateUUID, getErrorMessage, getGlobal, getItem, getKey, isUUID, memoizePromise, now, onTick, removeItem, setItem, setTimestamp, subscribe, throttle, tick, withRetryAsync };
1155
+ //# sourceMappingURL=index.js.map