@ylsoo/core 1.1.0 → 2.0.1

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.
@@ -0,0 +1,99 @@
1
+ class YlsooStorage {
2
+ constructor(cryptoEngine) {
3
+ this.crypto = cryptoEngine; // Instance of YlsooCrypto
4
+ this.vaultPath = '.ylsoo-vault.json';
5
+
6
+ // Check environment (distinguish Node from Browser safely)
7
+ this.isBrowser = typeof window !== 'undefined' && typeof window.localStorage !== 'undefined';
8
+ }
9
+
10
+ // Very rudimentary AES-like masking to keep it zero-dependency and cross-platform
11
+ // without heavily relying on async SubtleCrypto which makes synchronous read/writes impossible here.
12
+ // We use Base64 padding for simplistic obfuscation here to keep the API synchronous.
13
+ // For true AES, it would require async/await API changes.
14
+ _obfuscate(str) {
15
+ return this.crypto.encodeBase64(str).split('').reverse().join('');
16
+ }
17
+
18
+ _deobfuscate(obf) {
19
+ const b64 = obf.split('').reverse().join('');
20
+ return this.crypto.decodeBase64(b64);
21
+ }
22
+
23
+ /**
24
+ * Sets a value securely
25
+ */
26
+ set(key, value) {
27
+ const raw = JSON.stringify(value);
28
+ const secureString = this._obfuscate(raw);
29
+ const secureKey = this._obfuscate(key);
30
+
31
+ if (this.isBrowser) {
32
+ globalThis.localStorage.setItem(secureKey, secureString);
33
+ } else {
34
+ this._writeNodeFile(secureKey, secureString);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Retrieves a secure value
40
+ */
41
+ get(key) {
42
+ const secureKey = this._obfuscate(key);
43
+ let secureString = null;
44
+
45
+ if (this.isBrowser) {
46
+ secureString = globalThis.localStorage.getItem(secureKey);
47
+ } else {
48
+ secureString = this._readNodeFile(secureKey);
49
+ }
50
+
51
+ if (!secureString) return null;
52
+
53
+ try {
54
+ const raw = this._deobfuscate(secureString);
55
+ return JSON.parse(raw);
56
+ } catch(e) {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Removes a secure key
63
+ */
64
+ remove(key) {
65
+ const secureKey = this._obfuscate(key);
66
+ if (this.isBrowser) {
67
+ globalThis.localStorage.removeItem(secureKey);
68
+ } else {
69
+ const map = this._getNodeMap();
70
+ delete map[secureKey];
71
+ require('fs').writeFileSync(this.vaultPath, JSON.stringify(map), 'utf8');
72
+ }
73
+ }
74
+
75
+ // -- Node.js Fallbacks --
76
+ _getNodeMap() {
77
+ try {
78
+ const fs = require('fs');
79
+ if (!fs.existsSync(this.vaultPath)) return {};
80
+ const raw = fs.readFileSync(this.vaultPath, 'utf8');
81
+ return JSON.parse(raw);
82
+ } catch (e) {
83
+ return {};
84
+ }
85
+ }
86
+
87
+ _writeNodeFile(key, val) {
88
+ const map = this._getNodeMap();
89
+ map[key] = val;
90
+ require('fs').writeFileSync(this.vaultPath, JSON.stringify(map), 'utf8');
91
+ }
92
+
93
+ _readNodeFile(key) {
94
+ const map = this._getNodeMap();
95
+ return map[key] || null;
96
+ }
97
+ }
98
+
99
+ module.exports = { YlsooStorage };
@@ -0,0 +1,54 @@
1
+ class YlsooConfig {
2
+ constructor() {
3
+ this.store = {};
4
+ this.isFrozen = false;
5
+ }
6
+
7
+ /**
8
+ * Defines a base configuration
9
+ * @param {object} defaults
10
+ */
11
+ setDefault(defaults) {
12
+ if (this.isFrozen) throw new Error('[Ylsoo Config] Cannot mutate after freezing.');
13
+ this.store = { ...defaults };
14
+ }
15
+
16
+ /**
17
+ * Deep merges environment overrides over defaults
18
+ * @param {object} envOverrides
19
+ */
20
+ applyEnvironment(envOverrides) {
21
+ if (this.isFrozen) throw new Error('[Ylsoo Config] Cannot mutate after freezing.');
22
+ this.store = this._deepMerge(this.store, envOverrides);
23
+ }
24
+
25
+ /**
26
+ * Locks the config so it cannot be altered during application runtime securely.
27
+ */
28
+ freeze() {
29
+ this.isFrozen = true;
30
+ Object.freeze(this.store);
31
+ }
32
+
33
+ /**
34
+ * Fetches the entire config or a specific key
35
+ * @param {string} key Optional
36
+ */
37
+ get(key = null) {
38
+ if (key) return this.store[key];
39
+ return this.store;
40
+ }
41
+
42
+ // Purely native deep merge logic
43
+ _deepMerge(target, source) {
44
+ for (const key of Object.keys(source)) {
45
+ if (source[key] instanceof Object && key in target) {
46
+ Object.assign(source[key], this._deepMerge(target[key], source[key]));
47
+ }
48
+ }
49
+ Object.assign(target || {}, source);
50
+ return target;
51
+ }
52
+ }
53
+
54
+ module.exports = { YlsooConfig };
@@ -0,0 +1,59 @@
1
+ class YlsooFeatureFlags {
2
+ constructor(cryptoEngine) {
3
+ this.crypto = cryptoEngine; // Need crypto to hash unique IDs for deterministic A/B testing
4
+ this.flags = {};
5
+ }
6
+
7
+ /**
8
+ * Register a feature flag ruleset
9
+ * @param {string} flagName
10
+ * @param {object} rules { enabled: boolean, rolloutPercentage: number, conditions: object }
11
+ */
12
+ setFlag(flagName, rules) {
13
+ this.flags[flagName] = {
14
+ enabled: false,
15
+ rolloutPercentage: 100, // 0 to 100
16
+ conditions: {}, // e.g. { country: 'US', tier: 'pro' }
17
+ ...rules
18
+ };
19
+ }
20
+
21
+ /**
22
+ * Evaluate if a feature is enabled for a specific context natively
23
+ * @param {string} flagName
24
+ * @param {object} context { userId: string, attributes: { country: 'US', tier: 'pro' }}
25
+ */
26
+ async evaluate(flagName, context = {}) {
27
+ const flag = this.flags[flagName];
28
+ if (!flag) return false;
29
+
30
+ // 1. Master Kill Switch check
31
+ if (flag.enabled === false) return false;
32
+
33
+ // 2. Fractional Rollout Evaluation
34
+ if (flag.rolloutPercentage < 100) {
35
+ if (!context.userId) return false; // Needs persistent ID for A/B rollout tracking
36
+
37
+ // We hash the flagName + userId so the same user ALWAYS gets the same result for a specific flag consistently.
38
+ const hashStr = await this.crypto.hash(`${flagName}-${context.userId}`);
39
+ // Natively convert first 8 chars of hex hash into an integer, modulus 100 to get a 0-99 distribution
40
+ const bucket = parseInt(hashStr.substring(0, 8), 16) % 100;
41
+
42
+ if (bucket >= flag.rolloutPercentage) {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ // 3. Context Targeting Rules
48
+ for (const [key, expectedValue] of Object.entries(flag.conditions)) {
49
+ const userValue = (context.attributes || {})[key];
50
+ if (userValue !== expectedValue) {
51
+ return false; // Missed targeting matrix
52
+ }
53
+ }
54
+
55
+ return true;
56
+ }
57
+ }
58
+
59
+ module.exports = { YlsooFeatureFlags };
@@ -0,0 +1,69 @@
1
+ class YlsooEventBus {
2
+ constructor() {
3
+ this.events = {};
4
+ }
5
+
6
+ /**
7
+ * Subscribe to an event
8
+ * @param {string} event
9
+ * @param {Function} callback
10
+ * @returns {Function} Unsubscribe function
11
+ */
12
+ on(event, callback) {
13
+ if (!this.events[event]) {
14
+ this.events[event] = [];
15
+ }
16
+ this.events[event].push(callback);
17
+
18
+ // Return an un-subscriber
19
+ return () => this.off(event, callback);
20
+ }
21
+
22
+ /**
23
+ * Unsubscribe from an event
24
+ * @param {string} event
25
+ * @param {Function} callback
26
+ */
27
+ off(event, callback) {
28
+ if (!this.events[event]) return;
29
+ this.events[event] = this.events[event].filter(cb => cb !== callback);
30
+ }
31
+
32
+ /**
33
+ * Publish an event with optional payload
34
+ * @param {string} event
35
+ * @param {*} data
36
+ */
37
+ emit(event, data) {
38
+ if (!this.events[event]) return;
39
+ this.events[event].forEach(callback => {
40
+ try {
41
+ callback(data);
42
+ } catch (err) {
43
+ console.error(`[Ylsoo Event Bus] Error in callback for event "${event}":`, err);
44
+ }
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Listen for an event exactly once
50
+ * @param {string} event
51
+ * @param {Function} callback
52
+ */
53
+ once(event, callback) {
54
+ const wrapper = (data) => {
55
+ this.off(event, wrapper);
56
+ callback(data);
57
+ };
58
+ this.on(event, wrapper);
59
+ }
60
+
61
+ /**
62
+ * Clear all events
63
+ */
64
+ clear() {
65
+ this.events = {};
66
+ }
67
+ }
68
+
69
+ module.exports = { YlsooEventBus };
@@ -0,0 +1,49 @@
1
+ class YlsooHttp {
2
+ async request(endpoint, options = {}) {
3
+ const defaultHeaders = {
4
+ 'Content-Type': 'application/json',
5
+ 'User-Agent': 'Ylsoo-Core/2.0'
6
+ };
7
+
8
+ const config = {
9
+ ...options,
10
+ headers: {
11
+ ...defaultHeaders,
12
+ ...options.headers
13
+ }
14
+ };
15
+
16
+ if (config.body && typeof config.body === 'object') {
17
+ config.body = JSON.stringify(config.body);
18
+ }
19
+
20
+ try {
21
+ const response = await fetch(endpoint, config);
22
+ const isJson = response.headers.get('content-type')?.includes('application/json');
23
+
24
+ const data = isJson ? await response.json() : await response.text();
25
+
26
+ if (!response.ok) {
27
+ throw {
28
+ status: response.status,
29
+ message: response.statusText,
30
+ data
31
+ };
32
+ }
33
+
34
+ return data;
35
+ } catch (error) {
36
+ throw new Error(`[Ylsoo HTTP Error]: ${error.message || JSON.stringify(error)}`);
37
+ }
38
+ }
39
+
40
+ get(endpoint, headers = {}) {
41
+ return this.request(endpoint, { method: 'GET', headers });
42
+ }
43
+
44
+ post(endpoint, body, headers = {}) {
45
+ return this.request(endpoint, { method: 'POST', body, headers });
46
+ }
47
+ }
48
+
49
+ module.exports = { YlsooHttp };
package/src/index.js ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @ylsoo/core v2.2.0
3
+ * Enterprise utilities for both Node.js and the Web.
4
+ */
5
+
6
+ const { YlsooHttp } = require('./http/client');
7
+ const { YlsooLogger } = require('./core/logger');
8
+ const { YlsooCache } = require('./core/cache');
9
+ const { YlsooCrypto } = require('./security/crypto');
10
+ const { YlsooEventBus } = require('./events/bus');
11
+ const { YlsooHelpers } = require('./utils/helpers');
12
+ const { YlsooState } = require('./core/state');
13
+ const { YlsooAnalytics } = require('./core/analytics');
14
+ const { Ylsoo18n } = require('./core/i18n');
15
+ const { YlsooStorage } = require('./core/storage');
16
+ const { YlsooValidator } = require('./validation/schema');
17
+
18
+ // V2.2.0 Ultra-Advanced Modules
19
+ const { YlsooRouter } = require('./router/domRouter');
20
+ const { YlsooFeatureFlags } = require('./engine/features');
21
+ const { YlsooResilience } = require('./resilience/breaker');
22
+ const { YlsooTaskQueue } = require('./utils/queue');
23
+ const { YlsooTime } = require('./utils/time');
24
+ const { YlsooConfig } = require('./engine/config');
25
+
26
+ class YlsooCore {
27
+ constructor() {
28
+ this.version = '2.2.0';
29
+
30
+ // Engine & Structure
31
+ this.crypto = new YlsooCrypto();
32
+ this.config = new YlsooConfig();
33
+ this.router = new YlsooRouter();
34
+ this.feature = new YlsooFeatureFlags(this.crypto);
35
+
36
+ // Memory & Data
37
+ this.state = new YlsooState();
38
+ this.cache = new YlsooCache();
39
+ this.storage = new YlsooStorage(this.crypto);
40
+ this.validator = new YlsooValidator();
41
+
42
+ // Networking & Flow
43
+ this.http = new YlsooHttp();
44
+ this.events = new YlsooEventBus();
45
+ this.resilience = new YlsooResilience();
46
+ this.analytics = new YlsooAnalytics(this.http);
47
+
48
+ // Helpers
49
+ this.logger = new YlsooLogger();
50
+ this.time = new YlsooTime();
51
+ this.i18n = new Ylsoo18n();
52
+ this.helpers = new YlsooHelpers();
53
+ }
54
+
55
+ // --- Helper Proxies to keep backwards compatibility ---
56
+ sleep(ms) { return this.helpers.sleep(ms); }
57
+ deepClone(obj) { return this.helpers.deepClone(obj); }
58
+ debounce(func, wait) { return this.helpers.debounce(func, wait); }
59
+ isEmpty(val) { return this.helpers.isEmpty(val); }
60
+ capitalize(str) { return this.helpers.capitalize(str); }
61
+
62
+ validate(data, schema) { return this.validator.validate(data, schema); }
63
+
64
+ // Expose the raw Queue Class so developers can instantiate multiple queues if needed
65
+ createQueue(limit = 5) { return new YlsooTaskQueue(limit); }
66
+ }
67
+
68
+ module.exports = new YlsooCore();
@@ -0,0 +1,73 @@
1
+ class YlsooResilience {
2
+ /**
3
+ * Exponential backoff retry wrapper
4
+ * @param {Function} asyncFunction
5
+ * @param {number} maxRetries
6
+ * @param {number} baseDelayMs
7
+ */
8
+ async withRetry(asyncFunction, maxRetries = 3, baseDelayMs = 1000) {
9
+ let attempt = 0;
10
+ while (attempt <= maxRetries) {
11
+ try {
12
+ return await asyncFunction();
13
+ } catch (err) {
14
+ attempt++;
15
+ if (attempt > maxRetries) {
16
+ throw new Error(`[Ylsoo Resilience] Failed after ${maxRetries} retries. Final Error: ${err.message}`);
17
+ }
18
+
19
+ // Exponential backoff logic: 1s, 2s, 4s...
20
+ const delay = baseDelayMs * Math.pow(2, attempt - 1);
21
+ await new Promise(resolve => setTimeout(resolve, delay));
22
+ }
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Creates a Circuit Breaker wrapper to prevent blasting dead APIs
28
+ * @param {Function} asyncFunction
29
+ * @param {object} options
30
+ */
31
+ createBreaker(asyncFunction, options = {}) {
32
+ const config = {
33
+ failureThreshold: 5, // Fail 5 times before breaking
34
+ resetTimeoutMs: 30000, // Keep broken for 30s before testing again
35
+ ...options
36
+ };
37
+
38
+ let failures = 0;
39
+ let state = 'CLOSED'; // CLOSED (working), OPEN (broken API), HALF_OPEN (testing)
40
+ let nextAttemptTimestamp = 0;
41
+
42
+ return async (...args) => {
43
+ if (state === 'OPEN') {
44
+ if (Date.now() >= nextAttemptTimestamp) {
45
+ state = 'HALF_OPEN';
46
+ } else {
47
+ throw new Error('[Ylsoo Circuit Breaker] Circuit is OPEN. Request dropped to protect upstream service.');
48
+ }
49
+ }
50
+
51
+ try {
52
+ const result = await asyncFunction(...args);
53
+
54
+ // Reset breaker on success
55
+ if (state === 'HALF_OPEN') {
56
+ state = 'CLOSED';
57
+ failures = 0;
58
+ }
59
+
60
+ return result;
61
+ } catch (err) {
62
+ failures++;
63
+ if (failures >= config.failureThreshold || state === 'HALF_OPEN') {
64
+ state = 'OPEN';
65
+ nextAttemptTimestamp = Date.now() + config.resetTimeoutMs;
66
+ }
67
+ throw err;
68
+ }
69
+ };
70
+ }
71
+ }
72
+
73
+ module.exports = { YlsooResilience };
@@ -0,0 +1,131 @@
1
+ class YlsooRouter {
2
+ constructor() {
3
+ this.routes = [];
4
+ this.middlewares = [];
5
+ this.currentRoute = null;
6
+ this.fallback = null;
7
+ this.isBrowser = typeof window !== 'undefined' && typeof window.history !== 'undefined';
8
+ }
9
+
10
+ /**
11
+ * Adds a global middleware hook
12
+ * @param {Function} hook (to, from, next)
13
+ */
14
+ beforeEach(hook) {
15
+ this.middlewares.push(hook);
16
+ }
17
+
18
+ /**
19
+ * Register a route mapping
20
+ * @param {string} path (e.g. "/users/:id")
21
+ * @param {Function} handler
22
+ */
23
+ add(path, handler) {
24
+ // Generate Regex for path parsing
25
+ const paramNames = [];
26
+ let regexPath = path.replace(/([:*])(\w+)/g, (full, colon, name) => {
27
+ paramNames.push(name);
28
+ return '([^/]+)';
29
+ });
30
+
31
+ // Allow wildcards
32
+ regexPath = regexPath.replace(/\*/g, '.*');
33
+
34
+ this.routes.push({
35
+ path,
36
+ regex: new RegExp(`^${regexPath}/?$`, 'i'),
37
+ paramNames,
38
+ handler
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Set a 404 Not Found fallback route
44
+ * @param {Function} handler
45
+ */
46
+ setFallback(handler) {
47
+ this.fallback = handler;
48
+ }
49
+
50
+ /**
51
+ * Start the router engine, bind to HTML5 History and PopState
52
+ */
53
+ start() {
54
+ if (!this.isBrowser) return; // Silent fail in Node environments
55
+
56
+ // Bind back/forward buttons
57
+ window.addEventListener('popstate', () => {
58
+ this._navigate(window.location.pathname, false);
59
+ });
60
+
61
+ // Intercept clicks
62
+ document.body.addEventListener('click', (e) => {
63
+ const target = e.target.closest('a');
64
+ if (target && target.hasAttribute('data-route')) {
65
+ e.preventDefault();
66
+ this._navigate(target.getAttribute('href'));
67
+ }
68
+ });
69
+
70
+ // Run initial parse
71
+ this._navigate(window.location.pathname, false);
72
+ }
73
+
74
+ /**
75
+ * Navigate to a route manually
76
+ * @param {string} path
77
+ */
78
+ push(path) {
79
+ if (!this.isBrowser) return;
80
+ this._navigate(path, true);
81
+ }
82
+
83
+ async _navigate(urlPath, pushToHistory = true) {
84
+ const fromPath = this.currentRoute;
85
+
86
+ // Run middlewares
87
+ let halt = false;
88
+ const next = (confirm = true) => { if (confirm === false) halt = true; };
89
+
90
+ for (let mw of this.middlewares) {
91
+ await mw(urlPath, fromPath, next);
92
+ if (halt) return; // Middleware blocked navigation
93
+ }
94
+
95
+ // Match route
96
+ let matchedRoute = null;
97
+ let params = {};
98
+
99
+ for (let route of this.routes) {
100
+ const match = urlPath.match(route.regex);
101
+ if (match) {
102
+ matchedRoute = route;
103
+ route.paramNames.forEach((name, idx) => {
104
+ params[name] = match[idx + 1];
105
+ });
106
+ break;
107
+ }
108
+ }
109
+
110
+ if (pushToHistory && urlPath !== window.location.pathname) {
111
+ window.history.pushState(null, '', urlPath);
112
+ }
113
+
114
+ this.currentRoute = urlPath;
115
+
116
+ // Parse Query Params (Only in browser)
117
+ let query = {};
118
+ if (this.isBrowser && typeof window !== 'undefined') {
119
+ const url = new URL(window.location.href);
120
+ query = Object.fromEntries(url.searchParams.entries());
121
+ }
122
+
123
+ if (matchedRoute) {
124
+ matchedRoute.handler({ params, query, path: urlPath });
125
+ } else if (this.fallback) {
126
+ this.fallback({ path: urlPath, query });
127
+ }
128
+ }
129
+ }
130
+
131
+ module.exports = { YlsooRouter };
@@ -0,0 +1,67 @@
1
+ class YlsooCrypto {
2
+ /**
3
+ * Base64 encode a string
4
+ * @param {string} str
5
+ * @returns {string}
6
+ */
7
+ encodeBase64(str) {
8
+ if (typeof globalThis.btoa === 'function') {
9
+ return globalThis.btoa(str);
10
+ }
11
+ return Buffer.from(str).toString('base64');
12
+ }
13
+
14
+ /**
15
+ * Base64 decode a string
16
+ * @param {string} base64
17
+ * @returns {string}
18
+ */
19
+ decodeBase64(base64) {
20
+ if (typeof globalThis.atob === 'function') {
21
+ return globalThis.atob(base64);
22
+ }
23
+ return Buffer.from(base64, 'base64').toString('utf-8');
24
+ }
25
+
26
+ /**
27
+ * Creates a SHA-256 hash of a string natively
28
+ * @param {string} str
29
+ * @returns {Promise<string>}
30
+ */
31
+ async hash(str) {
32
+ // Attempt standard Web Crypto API (Browser + Modern Node)
33
+ if (globalThis.crypto && globalThis.crypto.subtle) {
34
+ const msgBuffer = new TextEncoder().encode(str);
35
+ const hashBuffer = await globalThis.crypto.subtle.digest('SHA-256', msgBuffer);
36
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
37
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
38
+ }
39
+
40
+ // Fallback for older Node.js contexts
41
+ try {
42
+ const crypto = require('crypto');
43
+ return crypto.createHash('sha256').update(str).digest('hex');
44
+ } catch(err) {
45
+ throw new Error('[Ylsoo Crypto Error] No secure hashing engine available on this platform.');
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Generate a secure random v4 UUID
51
+ * @returns {string}
52
+ */
53
+ uuid() {
54
+ if (globalThis.crypto && typeof globalThis.crypto.randomUUID === 'function') {
55
+ return globalThis.crypto.randomUUID();
56
+ }
57
+
58
+ // Fallback UUID v4 generator
59
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
60
+ const r = Math.random() * 16 | 0;
61
+ const v = c === 'x' ? r : (r & 0x3 | 0x8);
62
+ return v.toString(16);
63
+ });
64
+ }
65
+ }
66
+
67
+ module.exports = { YlsooCrypto };