@techstream/quark-core 1.1.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/utils.js ADDED
@@ -0,0 +1,219 @@
1
+ import crypto from "node:crypto";
2
+
3
+ /**
4
+ * @techstream/quark-core - Utility Functions
5
+ * Common utility functions used across the platform
6
+ */
7
+
8
+ /**
9
+ * Retries an async function with exponential backoff
10
+ * @param {Function} fn - Async function to retry
11
+ * @param {Object} options - Retry options
12
+ * @param {number} options.maxAttempts - Maximum number of attempts (default: 3)
13
+ * @param {number} options.initialDelay - Initial delay in ms (default: 1000)
14
+ * @param {number} options.maxDelay - Maximum delay in ms (default: 10000)
15
+ * @param {Function} options.onRetry - Callback on each retry
16
+ * @returns {Promise<any>} Result of the function
17
+ */
18
+ export const retryAsync = async (fn, options = {}) => {
19
+ const {
20
+ maxAttempts = 3,
21
+ initialDelay = 1000,
22
+ maxDelay = 10000,
23
+ onRetry = null,
24
+ } = options;
25
+
26
+ let lastError;
27
+
28
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
29
+ try {
30
+ return await fn();
31
+ } catch (error) {
32
+ lastError = error;
33
+
34
+ if (attempt < maxAttempts) {
35
+ const delay = Math.min(initialDelay * 2 ** (attempt - 1), maxDelay);
36
+ if (onRetry) {
37
+ onRetry({ attempt, delay, error });
38
+ }
39
+ await sleep(delay);
40
+ }
41
+ }
42
+ }
43
+
44
+ throw lastError;
45
+ };
46
+
47
+ /**
48
+ * Sleeps for a specified number of milliseconds
49
+ * @param {number} ms - Milliseconds to sleep
50
+ * @returns {Promise<void>}
51
+ */
52
+ export const sleep = (ms) => {
53
+ return new Promise((resolve) => setTimeout(resolve, ms));
54
+ };
55
+
56
+ /**
57
+ * Validates required environment variables
58
+ * @param {Array<string>} vars - Variable names to validate
59
+ * @throws {Error} If any variables are missing
60
+ * @returns {boolean} True if all variables exist
61
+ */
62
+ export const validateEnv = (vars) => {
63
+ const missing = vars.filter((v) => !process.env[v]);
64
+
65
+ if (missing.length > 0) {
66
+ throw new Error(
67
+ `Missing required environment variables: ${missing.join(", ")}`,
68
+ );
69
+ }
70
+
71
+ return true;
72
+ };
73
+
74
+ /**
75
+ * Deep merges two objects
76
+ * @param {Object} target - Target object
77
+ * @param {Object} source - Source object to merge
78
+ * @returns {Object} Merged object
79
+ */
80
+ export const deepMerge = (target, source) => {
81
+ const output = Object.assign({}, target);
82
+
83
+ if (isObject(target) && isObject(source)) {
84
+ Object.keys(source).forEach((key) => {
85
+ // Guard against prototype pollution
86
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
87
+ return;
88
+ }
89
+ if (isObject(source[key])) {
90
+ if (!(key in target)) {
91
+ Object.assign(output, { [key]: source[key] });
92
+ } else {
93
+ output[key] = deepMerge(target[key], source[key]);
94
+ }
95
+ } else {
96
+ Object.assign(output, { [key]: source[key] });
97
+ }
98
+ });
99
+ }
100
+
101
+ return output;
102
+ };
103
+
104
+ /**
105
+ * Checks if value is a plain object
106
+ * @param {any} item - Item to check
107
+ * @returns {boolean}
108
+ */
109
+ export const isObject = (item) => {
110
+ return item && typeof item === "object" && !Array.isArray(item);
111
+ };
112
+
113
+ /**
114
+ * @deprecated Use `getErrorMessage` from `@techstream/quark-core/errors` instead.
115
+ * Normalizes error messages for consistency
116
+ * @param {Error|string} error - Error to normalize
117
+ * @returns {string} Normalized error message
118
+ */
119
+ export { getErrorMessage as normalizeErrorMessage } from "./errors.js";
120
+
121
+ /**
122
+ * Generates a cryptographically secure random string of specified length.
123
+ * Uses crypto.randomBytes for secure randomness — safe for tokens and IDs.
124
+ * @param {number} length - String length
125
+ * @param {string} chars - Characters to use (default: alphanumeric)
126
+ * @returns {string} Random string
127
+ */
128
+ export const randomString = (
129
+ length = 16,
130
+ chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
131
+ ) => {
132
+ const bytes = crypto.randomBytes(length);
133
+ let result = "";
134
+ for (let i = 0; i < length; i++) {
135
+ result += chars.charAt(bytes[i] % chars.length);
136
+ }
137
+ return result;
138
+ };
139
+
140
+ /**
141
+ * Sanitizes a string for use in URLs or IDs
142
+ * @param {string} str - String to sanitize
143
+ * @returns {string} Sanitized string
144
+ */
145
+ export const sanitizeId = (str) => {
146
+ return str
147
+ .toLowerCase()
148
+ .replace(/[^a-z0-9]+/g, "-")
149
+ .replace(/^-+|-+$/g, "");
150
+ };
151
+
152
+ /**
153
+ * Formats bytes to human-readable size
154
+ * @param {number} bytes - Number of bytes
155
+ * @returns {string} Formatted size
156
+ */
157
+ export const formatBytes = (bytes) => {
158
+ if (bytes === 0) return "0 Bytes";
159
+
160
+ const k = 1024;
161
+ const sizes = ["Bytes", "KB", "MB", "GB"];
162
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
163
+
164
+ return `${Math.round((bytes / k ** i) * 100) / 100} ${sizes[i]}`;
165
+ };
166
+
167
+ /**
168
+ * Measures execution time of async function
169
+ * @param {Function} fn - Async function to measure
170
+ * @returns {Promise<Object>} { result, duration: ms }
171
+ */
172
+ export const measureTime = async (fn) => {
173
+ const start = performance.now();
174
+ const result = await fn();
175
+ const duration = performance.now() - start;
176
+
177
+ return { result, duration: Math.round(duration * 100) / 100 };
178
+ };
179
+
180
+ /**
181
+ * Creates a debounced function
182
+ * @param {Function} fn - Function to debounce
183
+ * @param {number} delay - Delay in milliseconds
184
+ * @returns {Function} Debounced function
185
+ */
186
+ export const debounce = (fn, delay = 300) => {
187
+ let timeoutId;
188
+
189
+ return (...args) => {
190
+ clearTimeout(timeoutId);
191
+ timeoutId = setTimeout(() => fn(...args), delay);
192
+ };
193
+ };
194
+
195
+ /**
196
+ * Creates a memoized function (caches results)
197
+ * @param {Function} fn - Function to memoize
198
+ * @param {number} ttl - Time to live in milliseconds (0 = no expiry)
199
+ * @returns {Function} Memoized function
200
+ */
201
+ export const memoize = (fn, ttl = 0) => {
202
+ const cache = new Map();
203
+
204
+ return (...args) => {
205
+ const key = JSON.stringify(args);
206
+
207
+ if (cache.has(key)) {
208
+ const cached = cache.get(key);
209
+ if (ttl === 0 || Date.now() - cached.timestamp < ttl) {
210
+ return cached.value;
211
+ }
212
+ cache.delete(key);
213
+ }
214
+
215
+ const value = fn(...args);
216
+ cache.set(key, { value, timestamp: Date.now() });
217
+ return value;
218
+ };
219
+ };
@@ -0,0 +1,193 @@
1
+ import assert from "node:assert";
2
+ import { test } from "node:test";
3
+ import {
4
+ debounce,
5
+ deepMerge,
6
+ formatBytes,
7
+ isObject,
8
+ memoize,
9
+ normalizeErrorMessage,
10
+ randomString,
11
+ retryAsync,
12
+ sanitizeId,
13
+ sleep,
14
+ validateEnv,
15
+ } from "../src/utils.js";
16
+
17
+ test("Utils Module", async (t) => {
18
+ await t.test("retryAsync succeeds on first attempt", async () => {
19
+ let attempts = 0;
20
+ const result = await retryAsync(async () => {
21
+ attempts++;
22
+ return "success";
23
+ });
24
+
25
+ assert(result === "success");
26
+ assert(attempts === 1);
27
+ });
28
+
29
+ await t.test("retryAsync retries on failure", async () => {
30
+ let attempts = 0;
31
+ const result = await retryAsync(
32
+ async () => {
33
+ attempts++;
34
+ if (attempts < 3) throw new Error("Fail");
35
+ return "success";
36
+ },
37
+ { maxAttempts: 5, initialDelay: 10 },
38
+ );
39
+
40
+ assert(result === "success");
41
+ assert(attempts === 3);
42
+ });
43
+
44
+ await t.test("retryAsync throws after max attempts", async () => {
45
+ let attempts = 0;
46
+ await assert.rejects(async () => {
47
+ await retryAsync(
48
+ async () => {
49
+ attempts++;
50
+ throw new Error("Always fails");
51
+ },
52
+ { maxAttempts: 3, initialDelay: 10 },
53
+ );
54
+ });
55
+
56
+ assert(attempts === 3);
57
+ });
58
+
59
+ await t.test("sleep delays execution", async () => {
60
+ const start = Date.now();
61
+ await sleep(50);
62
+ const elapsed = Date.now() - start;
63
+ assert(elapsed >= 40); // Allow some variance
64
+ });
65
+
66
+ await t.test("validateEnv checks required variables", () => {
67
+ process.env.TEST_VAR = "test";
68
+ assert(validateEnv(["TEST_VAR"]) === true);
69
+ });
70
+
71
+ await t.test("validateEnv throws on missing variables", () => {
72
+ assert.throws(() => {
73
+ validateEnv(["NONEXISTENT_VAR_12345"]);
74
+ });
75
+ });
76
+
77
+ await t.test("deepMerge merges objects", () => {
78
+ const result = deepMerge({ a: 1, b: { c: 2 } }, { b: { d: 3 }, e: 4 });
79
+
80
+ assert.deepStrictEqual(result, {
81
+ a: 1,
82
+ b: { c: 2, d: 3 },
83
+ e: 4,
84
+ });
85
+ });
86
+
87
+ await t.test("deepMerge handles nested objects", () => {
88
+ const result = deepMerge({ a: { b: { c: 1 } } }, { a: { b: { d: 2 } } });
89
+
90
+ assert.deepStrictEqual(result, {
91
+ a: { b: { c: 1, d: 2 } },
92
+ });
93
+ });
94
+
95
+ await t.test("isObject identifies objects", () => {
96
+ assert(isObject({}));
97
+ assert(isObject({ a: 1 }));
98
+ assert(!isObject([]));
99
+ assert(!isObject(null));
100
+ assert(!isObject("string"));
101
+ assert(!isObject(123));
102
+ });
103
+
104
+ await t.test("normalizeErrorMessage handles various types", () => {
105
+ assert(normalizeErrorMessage("string") === "string");
106
+ assert(normalizeErrorMessage(new Error("error")) === "error");
107
+ assert(normalizeErrorMessage({ message: "msg" }) === "msg");
108
+ assert(normalizeErrorMessage(null).includes("unknown"));
109
+ });
110
+
111
+ await t.test("randomString generates random strings", () => {
112
+ const str1 = randomString(16);
113
+ const str2 = randomString(16);
114
+
115
+ assert(str1.length === 16);
116
+ assert(str2.length === 16);
117
+ // Very unlikely to be equal
118
+ assert(str1 !== str2);
119
+ });
120
+
121
+ await t.test("randomString respects length parameter", () => {
122
+ assert(randomString(10).length === 10);
123
+ assert(randomString(50).length === 50);
124
+ });
125
+
126
+ await t.test("sanitizeId converts to valid IDs", () => {
127
+ assert(sanitizeId("Hello World") === "hello-world");
128
+ assert(sanitizeId("Test_123") === "test-123");
129
+ assert(sanitizeId("---hello---") === "hello");
130
+ assert(sanitizeId("UPPERCASE") === "uppercase");
131
+ });
132
+
133
+ await t.test("formatBytes formats file sizes", () => {
134
+ assert(formatBytes(0) === "0 Bytes");
135
+ assert(formatBytes(1024) === "1 KB");
136
+ assert(formatBytes(1024 * 1024) === "1 MB");
137
+ assert(formatBytes(1024 * 1024 * 1024) === "1 GB");
138
+ });
139
+
140
+ await t.test("debounce delays function execution", (_t, done) => {
141
+ let callCount = 0;
142
+ const debounced = debounce(() => {
143
+ callCount++;
144
+ }, 30);
145
+
146
+ debounced();
147
+ debounced();
148
+ debounced();
149
+
150
+ assert(callCount === 0); // Not called yet
151
+
152
+ setTimeout(() => {
153
+ assert(callCount === 1); // Called once after delay
154
+ done();
155
+ }, 50);
156
+ });
157
+
158
+ await t.test("memoize caches function results", () => {
159
+ let callCount = 0;
160
+ const memoized = memoize((x) => {
161
+ callCount++;
162
+ return x * 2;
163
+ });
164
+
165
+ assert(memoized(5) === 10);
166
+ assert(callCount === 1);
167
+ assert(memoized(5) === 10);
168
+ assert(callCount === 1); // Same result from cache
169
+
170
+ assert(memoized(10) === 20);
171
+ assert(callCount === 2); // Different input, new call
172
+ });
173
+
174
+ await t.test("memoize respects TTL", (_t, done) => {
175
+ let callCount = 0;
176
+ const memoized = memoize(
177
+ (x) => {
178
+ callCount++;
179
+ return x * 2;
180
+ },
181
+ 30, // 30ms TTL
182
+ );
183
+
184
+ assert(memoized(5) === 10);
185
+ assert(callCount === 1);
186
+
187
+ setTimeout(() => {
188
+ assert(memoized(5) === 10);
189
+ assert(callCount === 2); // Cache expired, new call
190
+ done();
191
+ }, 50);
192
+ });
193
+ });
@@ -0,0 +1,26 @@
1
+ import { ZodError } from "zod";
2
+ import { ValidationError } from "./errors.js";
3
+
4
+ /**
5
+ * Validates request body against a Zod schema
6
+ * @param {Request} request - Next.js Request object
7
+ * @param {import("zod").ZodSchema} schema - Zod schema to validate against
8
+ * @returns {Promise<any>} - Validated data
9
+ */
10
+ export async function validateBody(request, schema) {
11
+ let body;
12
+ try {
13
+ body = await request.json();
14
+ } catch (_err) {
15
+ throw new ValidationError("Invalid JSON body");
16
+ }
17
+
18
+ try {
19
+ return schema.parse(body);
20
+ } catch (error) {
21
+ if (error instanceof ZodError) {
22
+ throw new ValidationError("Validation failed", error.errors);
23
+ }
24
+ throw error;
25
+ }
26
+ }
@@ -0,0 +1,21 @@
1
+ // Test script - verify all core modules load
2
+ import("./src/auth/index.js").then(() => {
3
+ console.log("✅ Auth module loaded");
4
+ });
5
+
6
+ import("./src/errors.js").then(() => {
7
+ console.log("✅ Errors module loaded");
8
+ });
9
+
10
+ import("./src/utils.js").then(() => {
11
+ console.log("✅ Utils module loaded");
12
+ });
13
+
14
+ import("./src/queue/index.js").then(() => {
15
+ console.log("✅ Queue module loaded");
16
+ });
17
+
18
+ setTimeout(() => {
19
+ console.log("\n✨ All core modules are properly configured!");
20
+ process.exit(0);
21
+ }, 500);