@ylsoo/core 2.0.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.
- package/README.md +51 -53
- package/index.d.ts +87 -39
- package/package.json +8 -7
- package/src/core/analytics.js +66 -0
- package/src/core/i18n.js +42 -0
- package/src/core/state.js +67 -0
- package/src/core/storage.js +99 -0
- package/src/engine/config.js +54 -0
- package/src/engine/features.js +59 -0
- package/src/index.js +40 -8
- package/src/resilience/breaker.js +73 -0
- package/src/router/domRouter.js +131 -0
- package/src/utils/queue.js +50 -0
- package/src/utils/time.js +46 -0
- package/src/validation/schema.js +53 -0
package/README.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
<div align="center">
|
|
2
|
-
<h1>@ylsoo/core (v2.
|
|
3
|
-
<p><b>The
|
|
2
|
+
<h1>@ylsoo/core (v2.2 Enterprise)</h1>
|
|
3
|
+
<p><b>The absolute standard in enterprise SDK architecture.</b></p>
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@ylsoo/core)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
[](https://www.typescriptlang.org/)
|
|
8
|
-
[](#)
|
|
9
9
|
</div>
|
|
10
10
|
|
|
11
11
|
<hr>
|
|
12
12
|
|
|
13
13
|
## 🚀 Overview
|
|
14
14
|
|
|
15
|
-
`@ylsoo/core` v2
|
|
15
|
+
`@ylsoo/core` v2.2 introduces tier-1 routing capabilities and highly rigorous A/B target evaluation. Everything operates entirely on native standard technologies, avoiding the devastating payload bloat of common frameworks.
|
|
16
16
|
|
|
17
17
|
## 📦 Installation
|
|
18
18
|
|
|
@@ -20,74 +20,72 @@
|
|
|
20
20
|
npm i @ylsoo/core
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
## 🛠️
|
|
23
|
+
## 🛠️ V2.2 Enterprise Architecture Models
|
|
24
24
|
|
|
25
|
-
### 1.
|
|
26
|
-
|
|
27
|
-
```javascript
|
|
28
|
-
const ylsoo = require('@ylsoo/core');
|
|
29
|
-
|
|
30
|
-
// 1. Generate Secure V4 UUIDs
|
|
31
|
-
const sessionID = ylsoo.crypto.uuid();
|
|
32
|
-
|
|
33
|
-
// 2. Hash strings natively
|
|
34
|
-
const secretHash = await ylsoo.crypto.hash('my_super_secret');
|
|
25
|
+
### 1. 🛣️ Professional DOM Router (`ylsoo.router`)
|
|
26
|
+
An incredibly capable HTML5 Frontend matcher mimicking heavily advanced Vue/React dynamics.
|
|
35
27
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
- **Dynamic Constraints**: Extracts `/:id/` params cleanly into variables.
|
|
29
|
+
- **Middleware Control**: Provides `beforeEach()` to easily intercept and blockade navigations dynamically.
|
|
30
|
+
- **Click Hijacking**: Automatically binds to HTML5 PushState and catches `data-route` click events!
|
|
39
31
|
|
|
40
|
-
### 2. ⚡ Event Bus (`ylsoo.events`)
|
|
41
|
-
An enterprise Publisher/Subscriber mechanism to handle inter-component state within Ylsoo frontends or microservices.
|
|
42
32
|
```javascript
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
33
|
+
// 1. Build an Authentication Middleware Guard
|
|
34
|
+
ylsoo.router.beforeEach((to, from, next) => {
|
|
35
|
+
if (to.includes('admin') && !loggedIn) next(false); // Halt routing
|
|
46
36
|
});
|
|
47
37
|
|
|
48
|
-
//
|
|
49
|
-
ylsoo.
|
|
38
|
+
// 2. Configure Dynamic Deep Routes
|
|
39
|
+
ylsoo.router.add('/users/:id/billing', (route) => {
|
|
40
|
+
console.log('Loading Billing For User:', route.params.id);
|
|
41
|
+
});
|
|
50
42
|
|
|
51
|
-
|
|
52
|
-
ylsoo.events.once('system:boot', bootFunction);
|
|
43
|
+
ylsoo.router.start();
|
|
53
44
|
```
|
|
54
45
|
|
|
55
|
-
###
|
|
56
|
-
|
|
46
|
+
### 2. 🎛️ Deterministic A/B Flags (`ylsoo.feature`)
|
|
47
|
+
Not just a switch. A true matrix evaluation engine utilizing cryptographic hashing to guarantee specific users continuously load the exact same "random" feature buckets.
|
|
48
|
+
|
|
57
49
|
```javascript
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
50
|
+
// Force the dashboard to turn on ONLY for:
|
|
51
|
+
// 1) Exactly 25% of traffic
|
|
52
|
+
// 2) Users whose attributes dictate they live in the UK
|
|
53
|
+
ylsoo.feature.setFlag('beta_dashboard', {
|
|
54
|
+
enabled: true,
|
|
55
|
+
rolloutPercentage: 25,
|
|
56
|
+
conditions: { country: 'UK' }
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Execution check during login
|
|
60
|
+
const userContext = { userId: "uuid-123", attributes: { country: 'UK' }};
|
|
61
|
+
|
|
62
|
+
if (await ylsoo.feature.evaluate('beta_dashboard', userContext)) {
|
|
63
|
+
console.log('User Granted Beta Access');
|
|
64
64
|
}
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
###
|
|
68
|
-
|
|
67
|
+
### 3. 🛡️ Resilience Core (`ylsoo.resilience`)
|
|
68
|
+
- **`withRetry(fn)`**: Wraps flaky API calls and repeats them on an exponential delay loop (e.g. 1s... 2s... 4s...) rather than immediately panicking.
|
|
69
|
+
- **`createBreaker(fn)`**: Operates a Circuit Breaker algorithm to instantly cut network traffic down if an upstream service goes offline, protecting your node instances from dead-locking.
|
|
70
|
+
|
|
71
|
+
### 4. 🕐 Precise Time Formatter (`ylsoo.time`)
|
|
72
|
+
Completely strips the need for massive `moment.js` dependency packets. Quickly parses standard intervals.
|
|
69
73
|
```javascript
|
|
70
|
-
|
|
71
|
-
ylsoo.
|
|
72
|
-
const session = ylsoo.cache.get('session');
|
|
74
|
+
ylsoo.time.format(Date.now(), "YYYY-MM-DD HH:mm:ss");
|
|
75
|
+
ylsoo.time.timeAgo(oldDate); // "5 minutes ago"
|
|
73
76
|
```
|
|
74
77
|
|
|
75
|
-
### 5.
|
|
76
|
-
|
|
78
|
+
### 5. 🗄️ Strict Config Matrix (`ylsoo.config`)
|
|
79
|
+
A deep-merging configurations orchestrator. Implements `.freeze()` architecture to physically lock constants into memory, violently blocking any injected bad code downstream from maliciously altering your `API_KEYS`.
|
|
80
|
+
|
|
81
|
+
### 6. 🗃️ Concurrency Queue (`ylsoo.createQueue`)
|
|
82
|
+
Need thousands of promises to compute, but executing them simultaneously throttles your CPU? Assign them to a limit.
|
|
77
83
|
```javascript
|
|
78
|
-
ylsoo.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
ylsoo.logger.error('Critical failure detected.');
|
|
84
|
+
const q = ylsoo.createQueue(5); // Throttle process to exactly 5 lanes
|
|
85
|
+
q.add(() => fetchBigData1());
|
|
86
|
+
q.add(() => fetchBigData2());
|
|
82
87
|
```
|
|
83
88
|
|
|
84
|
-
### 6. 🧰 Native Enterprise Utilies
|
|
85
|
-
- **`ylsoo.sleep(ms)`**: Promise-based execution halter.
|
|
86
|
-
- **`ylsoo.deepClone(obj)`**: Safely deep clones utilizing native modern standards like `structuredClone` when available.
|
|
87
|
-
- **`ylsoo.debounce(func, wait)`**: High performance throttling mechanics.
|
|
88
|
-
- **`ylsoo.isEmpty(val)`**: Semantic entity verification (detects empty Sets, Maps, Objects, Strings, Arrays).
|
|
89
|
-
- **`ylsoo.capitalize(str)`**: Automatic string standardizer.
|
|
90
|
-
|
|
91
89
|
---
|
|
92
90
|
<div align="center">
|
|
93
91
|
<p>Built with ❤️ by <b>Ylsoo</b>.</p>
|
package/index.d.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @ylsoo/core v2.
|
|
3
|
-
* Cross-
|
|
2
|
+
* @ylsoo/core v2.2.0 Type Definitions
|
|
3
|
+
* Enterprise Cross-Platform SDK
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
declare module '@ylsoo/core' {
|
|
7
7
|
|
|
8
|
-
// ---
|
|
9
|
-
|
|
8
|
+
// --- Base Systems ---
|
|
10
9
|
export class YlsooLogger {
|
|
11
10
|
info(message: string | any): void;
|
|
12
11
|
success(message: string | any): void;
|
|
@@ -21,72 +20,121 @@ declare module '@ylsoo/core' {
|
|
|
21
20
|
clear(): void;
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
export interface YlsooHttpOptions {
|
|
25
|
-
method?: string;
|
|
26
|
-
headers?: Record<string, string>;
|
|
27
|
-
body?: any;
|
|
28
|
-
[key: string]: any;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
23
|
export class YlsooHttp {
|
|
32
|
-
request<T = any>(endpoint: string, options?:
|
|
33
|
-
get<T = any>(endpoint: string, headers?:
|
|
34
|
-
post<T = any>(endpoint: string, body: any, headers?:
|
|
24
|
+
request<T = any>(endpoint: string, options?: any): Promise<T>;
|
|
25
|
+
get<T = any>(endpoint: string, headers?: any): Promise<T>;
|
|
26
|
+
post<T = any>(endpoint: string, body: any, headers?: any): Promise<T>;
|
|
35
27
|
}
|
|
36
28
|
|
|
37
|
-
// --- Security
|
|
38
|
-
|
|
29
|
+
// --- Security & Process ---
|
|
39
30
|
export class YlsooCrypto {
|
|
40
|
-
/** Base64 encodes a utf-8 string securely */
|
|
41
31
|
encodeBase64(str: string): string;
|
|
42
|
-
|
|
43
|
-
/** Base64 decodes a string */
|
|
44
32
|
decodeBase64(base64: string): string;
|
|
45
|
-
|
|
46
|
-
/** Natively generates a secure SHA-256 hash */
|
|
47
33
|
hash(str: string): Promise<string>;
|
|
48
|
-
|
|
49
|
-
/** Generates a cryptographically strong v4 UUID */
|
|
50
34
|
uuid(): string;
|
|
51
35
|
}
|
|
52
36
|
|
|
53
|
-
// --- Event Modules ---
|
|
54
|
-
|
|
55
37
|
export class YlsooEventBus {
|
|
56
|
-
/** Subscribe to an event. Returns an unsubscribe function. */
|
|
57
38
|
on(event: string, callback: (data?: any) => void): () => void;
|
|
58
|
-
|
|
59
|
-
/** Unsubscribe from an event. */
|
|
60
39
|
off(event: string, callback: (data?: any) => void): void;
|
|
61
|
-
|
|
62
|
-
/** Publish an event to all subscribers. */
|
|
63
40
|
emit(event: string, data?: any): void;
|
|
64
|
-
|
|
65
|
-
/** Subscribe to an event, un-subscribing immediately after first emission. */
|
|
66
41
|
once(event: string, callback: (data?: any) => void): void;
|
|
67
|
-
|
|
68
|
-
/** clear all event listeners */
|
|
69
42
|
clear(): void;
|
|
70
43
|
}
|
|
71
44
|
|
|
72
|
-
// ---
|
|
45
|
+
// --- State & Config ---
|
|
46
|
+
export class YlsooState {
|
|
47
|
+
setup(key: string, initialValue?: any): void;
|
|
48
|
+
get<T = any>(key: string): T;
|
|
49
|
+
set(key: string, value: any): void;
|
|
50
|
+
subscribe<T = any>(key: string, callback: (val: T) => void): () => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class YlsooStorage {
|
|
54
|
+
set(key: string, value: any): void;
|
|
55
|
+
get<T = any>(key: string): T | null;
|
|
56
|
+
remove(key: string): void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class YlsooConfig {
|
|
60
|
+
setDefault(defaults: Record<string, any>): void;
|
|
61
|
+
applyEnvironment(envOverrides: Record<string, any>): void;
|
|
62
|
+
freeze(): void;
|
|
63
|
+
get<T = any>(key?: string | null): T;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class YlsooFeatureFlags {
|
|
67
|
+
setFlag(flagName: string, rules: { enabled?: boolean, rolloutPercentage?: number, conditions?: Record<string, any> }): void;
|
|
68
|
+
evaluate(flagName: string, context?: { userId?: string, attributes?: Record<string, any> }): Promise<boolean>;
|
|
69
|
+
}
|
|
73
70
|
|
|
71
|
+
// --- Flow & Routers ---
|
|
72
|
+
export class YlsooRouter {
|
|
73
|
+
beforeEach(hook: (to: string, from: string, next: (confirm?: boolean) => void) => void): void;
|
|
74
|
+
add(path: string, handler: (route: { params: any, query: any, path: string }) => void): void;
|
|
75
|
+
setFallback(handler: (route: { path: string, query: any }) => void): void;
|
|
76
|
+
start(): void;
|
|
77
|
+
push(path: string): void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export class YlsooResilience {
|
|
81
|
+
withRetry<T>(asyncFunction: () => Promise<T>, maxRetries?: number, baseDelayMs?: number): Promise<T>;
|
|
82
|
+
createBreaker<T>(asyncFunction: (...args: any[]) => Promise<T>, options?: any): (...args: any[]) => Promise<T>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export class YlsooTaskQueue {
|
|
86
|
+
constructor(concurrencyLimit?: number);
|
|
87
|
+
add<T>(asyncTask: () => Promise<T>): Promise<T>;
|
|
88
|
+
get remaining(): number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// --- Micro Utils ---
|
|
92
|
+
export class YlsooAnalytics {
|
|
93
|
+
configure(options: any): void;
|
|
94
|
+
track(eventName: string, metadata?: any): void;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export class Ylsoo18n {
|
|
98
|
+
load(locale: string, dict: Record<string, string>): void;
|
|
99
|
+
setLocale(locale: string): void;
|
|
100
|
+
t(key: string, variables?: Record<string, string>): string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export class YlsooTime {
|
|
104
|
+
format(date: Date | number | string, formatStr?: string): string | null;
|
|
105
|
+
timeAgo(date: Date | number | string): string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- Main Export ---
|
|
74
109
|
export class YlsooCore {
|
|
75
110
|
version: string;
|
|
76
111
|
|
|
77
|
-
|
|
78
|
-
|
|
112
|
+
crypto: YlsooCrypto;
|
|
113
|
+
config: YlsooConfig;
|
|
114
|
+
router: YlsooRouter;
|
|
115
|
+
feature: YlsooFeatureFlags;
|
|
116
|
+
|
|
117
|
+
state: YlsooState;
|
|
79
118
|
cache: YlsooCache;
|
|
119
|
+
storage: YlsooStorage;
|
|
120
|
+
|
|
80
121
|
http: YlsooHttp;
|
|
81
|
-
crypto: YlsooCrypto;
|
|
82
122
|
events: YlsooEventBus;
|
|
123
|
+
resilience: YlsooResilience;
|
|
124
|
+
analytics: YlsooAnalytics;
|
|
125
|
+
|
|
126
|
+
logger: YlsooLogger;
|
|
127
|
+
time: YlsooTime;
|
|
128
|
+
i18n: Ylsoo18n;
|
|
83
129
|
|
|
84
|
-
// Utilities
|
|
85
130
|
sleep(ms: number): Promise<void>;
|
|
86
131
|
deepClone<T>(obj: T): T;
|
|
87
132
|
debounce<T extends (...args: any[]) => any>(func: T, wait: number): (...args: Parameters<T>) => void;
|
|
88
133
|
isEmpty(value: any): boolean;
|
|
89
134
|
capitalize(str: string): string;
|
|
135
|
+
|
|
136
|
+
validate(data: any, schema: Record<string, any>): boolean;
|
|
137
|
+
createQueue(limit?: number): YlsooTaskQueue;
|
|
90
138
|
}
|
|
91
139
|
|
|
92
140
|
const ylsoo: YlsooCore;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ylsoo/core",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Enterprise cross-platform SDK for Ylsoo projects",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -17,12 +17,13 @@
|
|
|
17
17
|
"core",
|
|
18
18
|
"enterprise",
|
|
19
19
|
"sdk",
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
20
|
+
"router",
|
|
21
|
+
"feature-flags",
|
|
22
|
+
"ab-testing",
|
|
23
|
+
"circuit-breaker",
|
|
24
|
+
"retry",
|
|
25
|
+
"queue",
|
|
26
|
+
"time"
|
|
26
27
|
],
|
|
27
28
|
"author": "Ylsoo",
|
|
28
29
|
"license": "MIT",
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
class YlsooAnalytics {
|
|
2
|
+
constructor(httpEngine) {
|
|
3
|
+
this.http = httpEngine;
|
|
4
|
+
this.queue = [];
|
|
5
|
+
this.config = {
|
|
6
|
+
endpoint: null, // User must set this via configure()
|
|
7
|
+
batchSize: 10, // Flush after 10 events
|
|
8
|
+
flushInterval: 5000 // Or flush every 5 seconds
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
this.timer = null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
configure(options) {
|
|
15
|
+
this.config = { ...this.config, ...options };
|
|
16
|
+
this._startTimer();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Silently track an event
|
|
21
|
+
* @param {string} eventName
|
|
22
|
+
* @param {object} metadata
|
|
23
|
+
*/
|
|
24
|
+
track(eventName, metadata = {}) {
|
|
25
|
+
this.queue.push({
|
|
26
|
+
event: eventName,
|
|
27
|
+
data: metadata,
|
|
28
|
+
timestamp: new Date().toISOString()
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (this.queue.length >= this.config.batchSize) {
|
|
32
|
+
this.flush();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async flush() {
|
|
37
|
+
if (this.queue.length === 0 || !this.config.endpoint) return;
|
|
38
|
+
|
|
39
|
+
const payload = [...this.queue];
|
|
40
|
+
this.queue = []; // clear early to prevent race conditions
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// Intentionally suppress errors to stay silent
|
|
44
|
+
await this.http.post(this.config.endpoint, { events: payload });
|
|
45
|
+
} catch(err) {
|
|
46
|
+
// Re-queue on fail
|
|
47
|
+
this.queue = [...payload, ...this.queue];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_startTimer() {
|
|
52
|
+
if (this.timer) clearInterval(this.timer);
|
|
53
|
+
|
|
54
|
+
// In Node or Browser, setInterval is standard
|
|
55
|
+
this.timer = setInterval(() => {
|
|
56
|
+
this.flush();
|
|
57
|
+
}, this.config.flushInterval);
|
|
58
|
+
|
|
59
|
+
// In node, unref the timer to prevent keeping the process alive
|
|
60
|
+
if (typeof this.timer.unref === 'function') {
|
|
61
|
+
this.timer.unref();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { YlsooAnalytics };
|
package/src/core/i18n.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
class Ylsoo18n {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.dictionary = {};
|
|
4
|
+
this.locale = 'en';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Load a language dictionary into memory
|
|
9
|
+
* @param {string} locale
|
|
10
|
+
* @param {object} dict
|
|
11
|
+
*/
|
|
12
|
+
load(locale, dict) {
|
|
13
|
+
this.dictionary[locale] = dict;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
setLocale(locale) {
|
|
17
|
+
this.locale = locale;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Translate a key and interpolate variables
|
|
22
|
+
* @param {string} key
|
|
23
|
+
* @param {object} variables
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
t(key, variables = {}) {
|
|
27
|
+
const dict = this.dictionary[this.locale];
|
|
28
|
+
if (!dict || !dict[key]) return key; // Fallback to raw key
|
|
29
|
+
|
|
30
|
+
let translation = dict[key];
|
|
31
|
+
|
|
32
|
+
// Interpolate: "Welcome {name}" -> "Welcome Admin"
|
|
33
|
+
Object.keys(variables).forEach(varKey => {
|
|
34
|
+
const regex = new RegExp(`{${varKey}}`, 'g');
|
|
35
|
+
translation = translation.replace(regex, variables[varKey]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return translation;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { Ylsoo18n };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
class YlsooState {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.state = {};
|
|
4
|
+
this.listeners = new Map();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Initializes or updates a specific state key
|
|
9
|
+
* @param {string} key
|
|
10
|
+
* @param {*} initialValue
|
|
11
|
+
*/
|
|
12
|
+
setup(key, initialValue = null) {
|
|
13
|
+
if (!(key in this.state)) {
|
|
14
|
+
this.state[key] = initialValue;
|
|
15
|
+
this.listeners.set(key, new Set());
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Retrieves the current value of a state key
|
|
21
|
+
* @param {string} key
|
|
22
|
+
*/
|
|
23
|
+
get(key) {
|
|
24
|
+
return this.state[key];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Set a new value and trigger all subscribers
|
|
29
|
+
* @param {string} key
|
|
30
|
+
* @param {*} value
|
|
31
|
+
*/
|
|
32
|
+
set(key, value) {
|
|
33
|
+
this.setup(key); // Ensure setup
|
|
34
|
+
|
|
35
|
+
// Only trigger if actually changed to prevent render loops
|
|
36
|
+
if (this.state[key] !== value) {
|
|
37
|
+
this.state[key] = value;
|
|
38
|
+
this.listeners.get(key).forEach(callback => {
|
|
39
|
+
try {
|
|
40
|
+
callback(value);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.error(`[Ylsoo State] Error in listener for ${key}`, e);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Subscribe to changes on a specific key
|
|
50
|
+
* @param {string} key
|
|
51
|
+
* @param {Function} callback
|
|
52
|
+
* @returns {Function} Unsubscribe method
|
|
53
|
+
*/
|
|
54
|
+
subscribe(key, callback) {
|
|
55
|
+
this.setup(key);
|
|
56
|
+
this.listeners.get(key).add(callback);
|
|
57
|
+
|
|
58
|
+
// Provide initial state immediately upon subscription
|
|
59
|
+
callback(this.state[key]);
|
|
60
|
+
|
|
61
|
+
return () => {
|
|
62
|
+
this.listeners.get(key).delete(callback);
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { YlsooState };
|
|
@@ -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 };
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @ylsoo/core v2.
|
|
2
|
+
* @ylsoo/core v2.2.0
|
|
3
3
|
* Enterprise utilities for both Node.js and the Web.
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -9,28 +9,60 @@ const { YlsooCache } = require('./core/cache');
|
|
|
9
9
|
const { YlsooCrypto } = require('./security/crypto');
|
|
10
10
|
const { YlsooEventBus } = require('./events/bus');
|
|
11
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');
|
|
12
25
|
|
|
13
26
|
class YlsooCore {
|
|
14
27
|
constructor() {
|
|
15
|
-
this.version = '2.
|
|
28
|
+
this.version = '2.2.0';
|
|
16
29
|
|
|
17
|
-
//
|
|
18
|
-
this.
|
|
19
|
-
this.
|
|
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();
|
|
20
38
|
this.cache = new YlsooCache();
|
|
39
|
+
this.storage = new YlsooStorage(this.crypto);
|
|
40
|
+
this.validator = new YlsooValidator();
|
|
21
41
|
|
|
22
|
-
//
|
|
23
|
-
this.
|
|
42
|
+
// Networking & Flow
|
|
43
|
+
this.http = new YlsooHttp();
|
|
24
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();
|
|
25
52
|
this.helpers = new YlsooHelpers();
|
|
26
53
|
}
|
|
27
54
|
|
|
28
|
-
// --- Helper Proxies to keep backwards compatibility
|
|
55
|
+
// --- Helper Proxies to keep backwards compatibility ---
|
|
29
56
|
sleep(ms) { return this.helpers.sleep(ms); }
|
|
30
57
|
deepClone(obj) { return this.helpers.deepClone(obj); }
|
|
31
58
|
debounce(func, wait) { return this.helpers.debounce(func, wait); }
|
|
32
59
|
isEmpty(val) { return this.helpers.isEmpty(val); }
|
|
33
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); }
|
|
34
66
|
}
|
|
35
67
|
|
|
36
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,50 @@
|
|
|
1
|
+
class YlsooTaskQueue {
|
|
2
|
+
/**
|
|
3
|
+
* Limit concurrency of massive promises automatically
|
|
4
|
+
* @param {number} concurrencyLimit
|
|
5
|
+
*/
|
|
6
|
+
constructor(concurrencyLimit = 5) {
|
|
7
|
+
this.concurrencyLimit = concurrencyLimit;
|
|
8
|
+
this.activeCount = 0;
|
|
9
|
+
this.queue = [];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Add a function that returns a Promise to the queue
|
|
14
|
+
* @param {Function} asyncTask
|
|
15
|
+
* @returns {Promise<*>}
|
|
16
|
+
*/
|
|
17
|
+
add(asyncTask) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
this.queue.push({
|
|
20
|
+
task: asyncTask,
|
|
21
|
+
resolve,
|
|
22
|
+
reject
|
|
23
|
+
});
|
|
24
|
+
this._next();
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get remaining() {
|
|
29
|
+
return this.queue.length;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_next() {
|
|
33
|
+
if (this.activeCount >= this.concurrencyLimit || this.queue.length === 0) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.activeCount++;
|
|
38
|
+
const { task, resolve, reject } = this.queue.shift();
|
|
39
|
+
|
|
40
|
+
task()
|
|
41
|
+
.then(resolve)
|
|
42
|
+
.catch(reject)
|
|
43
|
+
.finally(() => {
|
|
44
|
+
this.activeCount--;
|
|
45
|
+
this._next();
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { YlsooTaskQueue };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
class YlsooTime {
|
|
2
|
+
/**
|
|
3
|
+
* Extremely lightweight date formatter (replaces heavy Moment.js)
|
|
4
|
+
* @param {Date|number|string} date
|
|
5
|
+
* @param {string} formatStr (e.g. "YYYY-MM-DD HH:mm:ss")
|
|
6
|
+
*/
|
|
7
|
+
format(date, formatStr = "YYYY-MM-DD") {
|
|
8
|
+
const d = new Date(date);
|
|
9
|
+
if (isNaN(d.getTime())) return null;
|
|
10
|
+
|
|
11
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
12
|
+
|
|
13
|
+
return formatStr
|
|
14
|
+
.replace('YYYY', d.getFullYear())
|
|
15
|
+
.replace('YY', String(d.getFullYear()).slice(-2))
|
|
16
|
+
.replace('MM', pad(d.getMonth() + 1))
|
|
17
|
+
.replace('DD', pad(d.getDate()))
|
|
18
|
+
.replace('HH', pad(d.getHours()))
|
|
19
|
+
.replace('mm', pad(d.getMinutes()))
|
|
20
|
+
.replace('ss', pad(d.getSeconds()));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Fast "time ago" algorithm
|
|
25
|
+
* @param {Date|number|string} date
|
|
26
|
+
*/
|
|
27
|
+
timeAgo(date) {
|
|
28
|
+
const d = new Date(date);
|
|
29
|
+
const seconds = Math.floor((new Date() - d) / 1000);
|
|
30
|
+
|
|
31
|
+
let interval = Math.floor(seconds / 31536000);
|
|
32
|
+
if (interval >= 1) return interval + " year" + (interval > 1 ? "s" : "") + " ago";
|
|
33
|
+
interval = Math.floor(seconds / 2592000);
|
|
34
|
+
if (interval >= 1) return interval + " month" + (interval > 1 ? "s" : "") + " ago";
|
|
35
|
+
interval = Math.floor(seconds / 86400);
|
|
36
|
+
if (interval >= 1) return interval + " day" + (interval > 1 ? "s" : "") + " ago";
|
|
37
|
+
interval = Math.floor(seconds / 3600);
|
|
38
|
+
if (interval >= 1) return interval + " hour" + (interval > 1 ? "s" : "") + " ago";
|
|
39
|
+
interval = Math.floor(seconds / 60);
|
|
40
|
+
if (interval >= 1) return interval + " minute" + (interval > 1 ? "s" : "") + " ago";
|
|
41
|
+
|
|
42
|
+
return Math.floor(seconds) + " seconds ago";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { YlsooTime };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
class YlsooValidator {
|
|
2
|
+
/**
|
|
3
|
+
* Validate an object against a strict schema mapping
|
|
4
|
+
* @param {*} data
|
|
5
|
+
* @param {object} schema
|
|
6
|
+
* @throws Error on validation failure
|
|
7
|
+
* @returns {boolean} true if valid
|
|
8
|
+
*/
|
|
9
|
+
validate(data, schema) {
|
|
10
|
+
if (typeof data !== 'object' || data === null) {
|
|
11
|
+
throw new Error('[Ylsoo Validation] Target is not an object.');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
for (const [key, rules] of Object.entries(schema)) {
|
|
15
|
+
const value = data[key];
|
|
16
|
+
|
|
17
|
+
// Handle raw type strings (e.g. { age: 'number' })
|
|
18
|
+
const type = typeof rules === 'string' ? rules : rules.type;
|
|
19
|
+
|
|
20
|
+
// Check required
|
|
21
|
+
const isRequired = typeof rules === 'object' ? rules.required !== false : true;
|
|
22
|
+
if (value === undefined || value === null) {
|
|
23
|
+
if (isRequired) throw new Error(`[Ylsoo Validation] Missing required key: ${key}`);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Check Types
|
|
28
|
+
if (type === 'array') {
|
|
29
|
+
if (!Array.isArray(value)) throw new Error(`[Ylsoo Validation] Type mismatch. ${key} must be an array.`);
|
|
30
|
+
} else if (type === 'email') {
|
|
31
|
+
if (typeof value !== 'string' || !/^\S+@\S+\.\S+$/.test(value)) {
|
|
32
|
+
throw new Error(`[Ylsoo Validation] Type mismatch. ${key} must be a valid email.`);
|
|
33
|
+
}
|
|
34
|
+
} else if (type === 'object') {
|
|
35
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
36
|
+
throw new Error(`[Ylsoo Validation] Type mismatch. ${key} must be an object.`);
|
|
37
|
+
}
|
|
38
|
+
// Recursive dive
|
|
39
|
+
if (rules.schema) {
|
|
40
|
+
this.validate(value, rules.schema);
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
if (typeof value !== type) {
|
|
44
|
+
throw new Error(`[Ylsoo Validation] Type mismatch. ${key} must be a ${type}.`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { YlsooValidator };
|