wu-framework 1.1.16 → 1.1.17

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.
@@ -1,68 +1,177 @@
1
1
  /**
2
- * 🚀 WU-LOADER: SISTEMA DE CARGA DINÁMICA UNIVERSAL
2
+ * WU-LOADER: SISTEMA DE CARGA DINAMICA UNIVERSAL
3
3
  * Carga aplicaciones y componentes sin depender del framework
4
+ *
5
+ * Cache strategy: LRU with TTL eviction.
6
+ * Entries track lastAccess time. When the cache reaches maxCacheSize,
7
+ * the least recently accessed entry is evicted. Entries older than
8
+ * cacheTTL are treated as stale and removed on access or eviction.
4
9
  */
5
10
 
6
11
  import { logger } from './wu-logger.js';
7
12
 
13
+ /**
14
+ * @typedef {Object} WuLoaderOptions
15
+ * @property {number} [maxCacheSize=50] - Maximum cache entries
16
+ * @property {number} [cacheTTL=1800000] - Cache TTL in ms (default 30min)
17
+ */
18
+
19
+ /**
20
+ * @typedef {Object} WuLoaderStats
21
+ * @property {number} cached - Number of cached entries
22
+ * @property {number} loading - Number of in-flight loads
23
+ * @property {number} maxCacheSize - Max cache size setting
24
+ * @property {number} cacheTTL - Cache TTL setting
25
+ * @property {string[]} cacheKeys - Cached URL keys
26
+ */
27
+
8
28
  export class WuLoader {
9
- constructor() {
29
+ /**
30
+ * @param {Object} options
31
+ * @param {number} [options.maxCacheSize=50] - Maximum number of entries in the cache
32
+ * @param {number} [options.cacheTTL=1800000] - Time-to-live for cache entries in ms (default 30 minutes)
33
+ */
34
+ constructor(options = {}) {
35
+ this.maxCacheSize = options.maxCacheSize ?? 50;
36
+ this.cacheTTL = options.cacheTTL ?? 1800000;
10
37
  this.cache = new Map();
11
38
  this.loadingPromises = new Map();
12
39
 
13
- logger.debug('[WuLoader] 📦 Dynamic loader initialized');
40
+ logger.debug('[WuLoader] Dynamic loader initialized');
41
+ }
42
+
43
+ /**
44
+ * Read from cache with TTL validation and LRU access tracking.
45
+ * Returns undefined if the entry does not exist or has expired.
46
+ * @param {string} key
47
+ * @returns {string|undefined}
48
+ */
49
+ _cacheGet(key) {
50
+ if (!this.cache.has(key)) {
51
+ return undefined;
52
+ }
53
+
54
+ const entry = this.cache.get(key);
55
+ const now = Date.now();
56
+
57
+ if (now - entry.timestamp > this.cacheTTL) {
58
+ this.cache.delete(key);
59
+ logger.debug(`[WuLoader] Cache expired for: ${key}`);
60
+ return undefined;
61
+ }
62
+
63
+ // Promote: delete and re-insert so iteration order reflects recency.
64
+ // Map iteration order in JS follows insertion order, so the oldest
65
+ // inserted entry is always first -- exactly what we need for LRU eviction.
66
+ this.cache.delete(key);
67
+ entry.lastAccess = now;
68
+ this.cache.set(key, entry);
69
+
70
+ return entry.code;
71
+ }
72
+
73
+ /**
74
+ * Write to cache. Evicts stale and LRU entries before inserting.
75
+ * @param {string} key
76
+ * @param {string} code
77
+ */
78
+ _cacheSet(key, code) {
79
+ // If the key already exists, remove it first so re-insertion
80
+ // moves it to the end (most-recently-used position).
81
+ if (this.cache.has(key)) {
82
+ this.cache.delete(key);
83
+ }
84
+
85
+ this._evictIfNeeded();
86
+
87
+ const now = Date.now();
88
+ this.cache.set(key, {
89
+ code,
90
+ timestamp: now,
91
+ lastAccess: now
92
+ });
93
+ }
94
+
95
+ /**
96
+ * Evict entries until cache is below maxCacheSize.
97
+ *
98
+ * Two-pass strategy:
99
+ * 1. Remove all expired entries (TTL exceeded).
100
+ * 2. If still at capacity, remove the least recently accessed entry.
101
+ * Because Map preserves insertion order and _cacheGet promotes on
102
+ * access, the first key from the iterator is always the LRU entry.
103
+ */
104
+ _evictIfNeeded() {
105
+ const now = Date.now();
106
+
107
+ // Pass 1: purge expired entries
108
+ for (const [key, entry] of this.cache) {
109
+ if (now - entry.timestamp > this.cacheTTL) {
110
+ this.cache.delete(key);
111
+ logger.debug(`[WuLoader] Evicted expired entry: ${key}`);
112
+ }
113
+ }
114
+
115
+ // Pass 2: evict LRU entries until we are under the limit
116
+ while (this.cache.size >= this.maxCacheSize) {
117
+ // Map.keys().next() gives us the oldest-inserted key (LRU)
118
+ const oldestKey = this.cache.keys().next().value;
119
+ this.cache.delete(oldestKey);
120
+ logger.debug(`[WuLoader] Evicted LRU entry: ${oldestKey}`);
121
+ }
14
122
  }
15
123
 
16
124
  /**
17
- * Cargar aplicación completa
18
- * @param {string} appUrl - URL base de la aplicación
19
- * @param {Object} manifest - Manifest de la aplicación
20
- * @returns {string} Código JavaScript de la aplicación
125
+ * Cargar aplicacion completa
126
+ * @param {string} appUrl - URL base de la aplicacion
127
+ * @param {Object} manifest - Manifest de la aplicacion
128
+ * @returns {string} Codigo JavaScript de la aplicacion
21
129
  */
22
130
  async loadApp(appUrl, manifest) {
23
131
  const entryFile = manifest?.entry || 'index.js';
24
132
  const fullUrl = `${appUrl}/${entryFile}`;
25
133
 
26
- logger.debug(`[WuLoader] 📥 Loading app from: ${fullUrl}`);
134
+ logger.debug(`[WuLoader] Loading app from: ${fullUrl}`);
27
135
 
28
136
  try {
29
- // Verificar cache
30
- if (this.cache.has(fullUrl)) {
31
- logger.debug(`[WuLoader] Cache hit for: ${fullUrl}`);
32
- return this.cache.get(fullUrl);
137
+ // Check cache with TTL and LRU tracking
138
+ const cached = this._cacheGet(fullUrl);
139
+ if (cached !== undefined) {
140
+ logger.debug(`[WuLoader] Cache hit for: ${fullUrl}`);
141
+ return cached;
33
142
  }
34
143
 
35
- // Verificar si ya está cargando
144
+ // Check if already loading
36
145
  if (this.loadingPromises.has(fullUrl)) {
37
- logger.debug(`[WuLoader] Loading in progress for: ${fullUrl}`);
146
+ logger.debug(`[WuLoader] Loading in progress for: ${fullUrl}`);
38
147
  return await this.loadingPromises.get(fullUrl);
39
148
  }
40
149
 
41
- // Crear promesa de carga
150
+ // Create loading promise
42
151
  const loadingPromise = this.fetchCode(fullUrl);
43
152
  this.loadingPromises.set(fullUrl, loadingPromise);
44
153
 
45
154
  const code = await loadingPromise;
46
155
 
47
- // Limpiar promesa de carga y cachear resultado
156
+ // Clean up loading promise and cache result
48
157
  this.loadingPromises.delete(fullUrl);
49
- this.cache.set(fullUrl, code);
158
+ this._cacheSet(fullUrl, code);
50
159
 
51
- logger.debug(`[WuLoader] App loaded successfully: ${fullUrl}`);
160
+ logger.debug(`[WuLoader] App loaded successfully: ${fullUrl}`);
52
161
  return code;
53
162
 
54
163
  } catch (error) {
55
164
  this.loadingPromises.delete(fullUrl);
56
- console.error(`[WuLoader] Failed to load app: ${fullUrl}`, error);
165
+ console.error(`[WuLoader] Failed to load app: ${fullUrl}`, error);
57
166
  throw new Error(`Failed to load app from ${fullUrl}: ${error.message}`);
58
167
  }
59
168
  }
60
169
 
61
170
  /**
62
- * Cargar componente específico
63
- * @param {string} appUrl - URL base de la aplicación
171
+ * Cargar componente especifico
172
+ * @param {string} appUrl - URL base de la aplicacion
64
173
  * @param {string} componentPath - Ruta del componente
65
- * @returns {Function} Función del componente
174
+ * @returns {Function} Funcion del componente
66
175
  */
67
176
  async loadComponent(appUrl, componentPath) {
68
177
  // Normalizar ruta del componente
@@ -76,13 +185,13 @@ export class WuLoader {
76
185
 
77
186
  const fullUrl = `${appUrl}/${normalizedPath}`;
78
187
 
79
- logger.debug(`[WuLoader] 🧩 Loading component from: ${fullUrl}`);
188
+ logger.debug(`[WuLoader] Loading component from: ${fullUrl}`);
80
189
 
81
190
  try {
82
- // Cargar código del componente
191
+ // Cargar codigo del componente
83
192
  const code = await this.loadCode(fullUrl);
84
193
 
85
- // Crear función que retorna el componente
194
+ // Crear funcion que retorna el componente
86
195
  const componentFunction = new Function('require', 'module', 'exports', `
87
196
  ${code}
88
197
  return typeof module.exports === 'function' ? module.exports :
@@ -99,39 +208,40 @@ export class WuLoader {
99
208
 
100
209
  const component = componentFunction(fakeRequire, fakeModule, fakeModule.exports);
101
210
 
102
- logger.debug(`[WuLoader] Component loaded: ${componentPath}`);
211
+ logger.debug(`[WuLoader] Component loaded: ${componentPath}`);
103
212
  return component;
104
213
 
105
214
  } catch (error) {
106
- console.error(`[WuLoader] Failed to load component: ${componentPath}`, error);
215
+ console.error(`[WuLoader] Failed to load component: ${componentPath}`, error);
107
216
  throw new Error(`Failed to load component ${componentPath}: ${error.message}`);
108
217
  }
109
218
  }
110
219
 
111
220
  /**
112
- * Cargar código con cache
221
+ * Cargar codigo con cache
113
222
  * @param {string} url - URL del archivo
114
- * @returns {string} Código JavaScript
223
+ * @returns {string} Codigo JavaScript
115
224
  */
116
225
  async loadCode(url) {
117
- // Verificar cache
118
- if (this.cache.has(url)) {
119
- return this.cache.get(url);
226
+ // Check cache with TTL and LRU tracking
227
+ const cached = this._cacheGet(url);
228
+ if (cached !== undefined) {
229
+ return cached;
120
230
  }
121
231
 
122
- // Verificar si ya está cargando
232
+ // Check if already loading
123
233
  if (this.loadingPromises.has(url)) {
124
234
  return await this.loadingPromises.get(url);
125
235
  }
126
236
 
127
- // Crear promesa de carga
237
+ // Create loading promise
128
238
  const loadingPromise = this.fetchCode(url);
129
239
  this.loadingPromises.set(url, loadingPromise);
130
240
 
131
241
  try {
132
242
  const code = await loadingPromise;
133
243
  this.loadingPromises.delete(url);
134
- this.cache.set(url, code);
244
+ this._cacheSet(url, code);
135
245
  return code;
136
246
  } catch (error) {
137
247
  this.loadingPromises.delete(url);
@@ -140,9 +250,9 @@ export class WuLoader {
140
250
  }
141
251
 
142
252
  /**
143
- * Realizar fetch del código
253
+ * Realizar fetch del codigo
144
254
  * @param {string} url - URL del archivo
145
- * @returns {string} Código JavaScript
255
+ * @returns {string} Codigo JavaScript
146
256
  */
147
257
  async fetchCode(url) {
148
258
  const response = await fetch(url, {
@@ -170,25 +280,25 @@ export class WuLoader {
170
280
  * @param {Array} appConfigs - Configuraciones de aplicaciones
171
281
  */
172
282
  async preload(appConfigs) {
173
- logger.debug(`[WuLoader] 🚀 Preloading ${appConfigs.length} apps...`);
283
+ logger.debug(`[WuLoader] Preloading ${appConfigs.length} apps...`);
174
284
 
175
285
  const preloadPromises = appConfigs.map(async (config) => {
176
286
  try {
177
287
  await this.loadApp(config.url, config.manifest);
178
- logger.debug(`[WuLoader] Preloaded: ${config.name}`);
288
+ logger.debug(`[WuLoader] Preloaded: ${config.name}`);
179
289
  } catch (error) {
180
- logger.warn(`[WuLoader] ⚠️ Failed to preload ${config.name}:`, error.message);
290
+ logger.warn(`[WuLoader] Failed to preload ${config.name}:`, error.message);
181
291
  }
182
292
  });
183
293
 
184
294
  await Promise.allSettled(preloadPromises);
185
- logger.debug(`[WuLoader] 🎉 Preload completed`);
295
+ logger.debug(`[WuLoader] Preload completed`);
186
296
  }
187
297
 
188
298
  /**
189
- * Verificar si una URL está disponible
299
+ * Verificar si una URL esta disponible
190
300
  * @param {string} url - URL a verificar
191
- * @returns {boolean} True si está disponible
301
+ * @returns {boolean} True si esta disponible
192
302
  */
193
303
  async isAvailable(url) {
194
304
  try {
@@ -232,9 +342,9 @@ export class WuLoader {
232
342
  try {
233
343
  const component = await this.loadComponent(app.url, exportPath);
234
344
  resolved.set(importPath, component);
235
- logger.debug(`[WuLoader] Resolved dependency: ${importPath}`);
345
+ logger.debug(`[WuLoader] Resolved dependency: ${importPath}`);
236
346
  } catch (error) {
237
- console.error(`[WuLoader] Failed to resolve: ${importPath}`, error);
347
+ console.error(`[WuLoader] Failed to resolve: ${importPath}`, error);
238
348
  }
239
349
  }
240
350
 
@@ -243,7 +353,7 @@ export class WuLoader {
243
353
 
244
354
  /**
245
355
  * Limpiar cache
246
- * @param {string} pattern - Patrón opcional para limpiar URLs específicas
356
+ * @param {string} pattern - Patron opcional para limpiar URLs especificas
247
357
  */
248
358
  clearCache(pattern) {
249
359
  if (pattern) {
@@ -251,23 +361,25 @@ export class WuLoader {
251
361
  for (const [url] of this.cache) {
252
362
  if (regex.test(url)) {
253
363
  this.cache.delete(url);
254
- logger.debug(`[WuLoader] 🗑️ Cleared cache for: ${url}`);
364
+ logger.debug(`[WuLoader] Cleared cache for: ${url}`);
255
365
  }
256
366
  }
257
367
  } else {
258
368
  this.cache.clear();
259
- logger.debug(`[WuLoader] 🗑️ Cache cleared completely`);
369
+ logger.debug(`[WuLoader] Cache cleared completely`);
260
370
  }
261
371
  }
262
372
 
263
373
  /**
264
- * Obtener estadísticas del loader
374
+ * Obtener estadisticas del loader
265
375
  */
266
376
  getStats() {
267
377
  return {
268
378
  cached: this.cache.size,
379
+ maxCacheSize: this.maxCacheSize,
380
+ cacheTTL: this.cacheTTL,
269
381
  loading: this.loadingPromises.size,
270
382
  cacheKeys: Array.from(this.cache.keys())
271
383
  };
272
384
  }
273
- }
385
+ }
@@ -23,19 +23,27 @@ export class WuLogger {
23
23
  * Detectar si estamos en desarrollo
24
24
  */
25
25
  detectEnvironment() {
26
- // Múltiples formas de detectar desarrollo
27
- return (
28
- // Vite development
29
- window.location.hostname === 'localhost' ||
30
- window.location.hostname === '127.0.0.1' ||
31
- window.location.port !== '' ||
32
- // NODE_ENV si está disponible
33
- (typeof process !== 'undefined' && process.env?.NODE_ENV === 'development') ||
34
- // URL params para forzar debug
35
- new URLSearchParams(window.location.search).has('wu-debug') ||
36
- // Manual override
37
- window.WU_DEBUG === true
38
- );
26
+ // 1. Explicit flag takes priority
27
+ if (typeof window !== 'undefined' && window.WU_DEBUG === true) return true;
28
+ if (typeof window !== 'undefined' && window.WU_DEBUG === false) return false;
29
+
30
+ // 2. NODE_ENV check (works in bundlers and Node)
31
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') return false;
32
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'development') return true;
33
+
34
+ // 3. Browser heuristics (only if window exists)
35
+ if (typeof window !== 'undefined' && window.location) {
36
+ const hostname = window.location.hostname;
37
+ if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]') return true;
38
+
39
+ // URL param override
40
+ try {
41
+ if (new URLSearchParams(window.location.search).has('wu-debug')) return true;
42
+ } catch {}
43
+ }
44
+
45
+ // 4. Default: assume production
46
+ return false;
39
47
  }
40
48
 
41
49
  /**
@@ -302,6 +302,29 @@ export class WuManifest {
302
302
  }
303
303
  }
304
304
 
305
+ // Validate optional fields
306
+ if (manifest.styleMode !== undefined) {
307
+ const validModes = ['shared', 'isolated', 'fully-isolated'];
308
+ if (!validModes.includes(manifest.styleMode)) {
309
+ logger.warn(`[WuManifest] Invalid styleMode "${manifest.styleMode}", defaulting to "shared". Valid: ${validModes.join(', ')}`);
310
+ manifest.styleMode = 'shared';
311
+ }
312
+ }
313
+
314
+ if (manifest.version !== undefined && typeof manifest.version !== 'string') {
315
+ logger.warn('[WuManifest] version must be a string, ignoring');
316
+ delete manifest.version;
317
+ }
318
+
319
+ if (manifest.folder !== undefined) {
320
+ if (typeof manifest.folder !== 'string') {
321
+ logger.warn('[WuManifest] folder must be a string, ignoring');
322
+ delete manifest.folder;
323
+ } else if (this._hasDangerousPatterns(manifest.folder)) {
324
+ throw new Error('folder contains dangerous patterns');
325
+ }
326
+ }
327
+
305
328
  // Normalizar y limpiar manifest
306
329
  return this.normalize(manifest);
307
330
  }
@@ -11,7 +11,7 @@ import { logger } from './wu-logger.js';
11
11
 
12
12
 
13
13
  export class WuPluginSystem {
14
- constructor(core) {
14
+ constructor(core, options = {}) {
15
15
  this._core = core; // Privado - no expuesto a plugins
16
16
  this.plugins = new Map();
17
17
  this.hooks = new Map();
@@ -35,7 +35,7 @@ export class WuPluginSystem {
35
35
  ];
36
36
 
37
37
  // 🔐 Timeout para hooks (evita que plugins bloqueen)
38
- this.hookTimeout = 5000; // 5 segundos
38
+ this.hookTimeout = options.hookTimeout || 5000; // Default: 5 segundos
39
39
 
40
40
  this.availableHooks.forEach(hook => {
41
41
  this.hooks.set(hook, []);
@@ -106,8 +106,61 @@ export class WuPluginSystem {
106
106
  logger.warn('[WuPlugin] ⚠️ Plugin has unsafe access to core!');
107
107
  }
108
108
 
109
- // Congelar API para evitar modificaciones
110
- return Object.freeze(api);
109
+ // Congelar API recursivamente para evitar modificaciones en cualquier nivel
110
+ return WuPluginSystem._deepFreeze(api);
111
+ }
112
+
113
+ /**
114
+ * Deep-freeze an object and all its nested plain objects and arrays.
115
+ * Functions, DOM nodes, and other non-plain types are left untouched
116
+ * so they remain callable / functional. A WeakSet guards against
117
+ * circular references.
118
+ *
119
+ * @param {*} obj - The value to deep-freeze
120
+ * @param {WeakSet} [seen] - Internal tracker for circular references
121
+ * @returns {*} The same object, now deeply frozen
122
+ */
123
+ static _deepFreeze(obj, seen) {
124
+ if (obj === null || obj === undefined) {
125
+ return obj;
126
+ }
127
+
128
+ // Only freeze plain objects and arrays.
129
+ // Functions must stay invocable; DOM nodes, class instances, etc.
130
+ // should not be tampered with.
131
+ const dominated = typeof obj === 'object';
132
+ if (!dominated) {
133
+ return obj;
134
+ }
135
+
136
+ const isPlainObject =
137
+ Object.getPrototypeOf(obj) === Object.prototype ||
138
+ Object.getPrototypeOf(obj) === null;
139
+ const isArray = Array.isArray(obj);
140
+
141
+ if (!isPlainObject && !isArray) {
142
+ return obj;
143
+ }
144
+
145
+ // Circular-reference guard
146
+ if (!seen) {
147
+ seen = new WeakSet();
148
+ }
149
+ if (seen.has(obj)) {
150
+ return obj;
151
+ }
152
+ seen.add(obj);
153
+
154
+ // Recurse into own enumerable properties
155
+ const keys = Object.keys(obj);
156
+ for (let i = 0; i < keys.length; i++) {
157
+ const value = obj[keys[i]];
158
+ if (value !== null && value !== undefined && typeof value === 'object') {
159
+ WuPluginSystem._deepFreeze(value, seen);
160
+ }
161
+ }
162
+
163
+ return Object.freeze(obj);
111
164
  }
112
165
 
113
166
  /**
@@ -13,8 +13,9 @@
13
13
  import { logger } from './wu-logger.js';
14
14
 
15
15
  export class WuProxySandbox {
16
- constructor(appName) {
16
+ constructor(appName, options = {}) {
17
17
  this.appName = appName;
18
+ this.options = options;
18
19
  this.proxy = null;
19
20
  this.fakeWindow = Object.create(null);
20
21
  this.active = false;
@@ -15,6 +15,52 @@ import { logger } from './wu-logger.js';
15
15
 
16
16
  export class WuScriptExecutor {
17
17
 
18
+ /**
19
+ * Dangerous patterns that indicate prototype pollution, sandbox escape,
20
+ * or direct access to sensitive APIs. Each entry is a regex paired with
21
+ * a human-readable label used in error messages.
22
+ *
23
+ * This is a tripwire, not a full parser. It catches the most common
24
+ * attack vectors without the overhead of AST analysis.
25
+ */
26
+ static DANGEROUS_PATTERNS = [
27
+ // Prototype pollution vectors
28
+ { pattern: /constructor\s*\[\s*['"`]constructor['"`]\s*\]/, label: 'constructor chain access (sandbox escape)' },
29
+ { pattern: /__proto__/, label: '__proto__ access (prototype pollution)' },
30
+
31
+ // Sandbox escape via proxy introspection
32
+ { pattern: /Object\s*\.\s*getPrototypeOf\s*\(\s*proxy\s*\)/, label: 'Object.getPrototypeOf(proxy) (sandbox escape)' },
33
+
34
+ // Dynamic code generation that bypasses the sandbox
35
+ { pattern: /Function\s*\(\s*['"`]/, label: 'Function() constructor (dynamic code generation)' },
36
+ { pattern: /\beval\s*\(/, label: 'eval() (dynamic code execution)' },
37
+
38
+ // Dynamic import escapes the sandbox entirely (runs in global scope)
39
+ { pattern: /\bimport\s*\(/, label: 'import() (dynamic import escapes sandbox)' },
40
+
41
+ // Direct cookie access (should go through proxy traps, not raw document)
42
+ { pattern: /document\s*\.\s*cookie/, label: 'document.cookie (direct cookie access)' },
43
+ ];
44
+
45
+ /**
46
+ * Validate script text against known dangerous patterns before execution.
47
+ * Throws if any pattern matches. This is intentionally lightweight --
48
+ * pattern detection only, not a full parse.
49
+ *
50
+ * @param {string} scriptText - The raw script to validate
51
+ * @param {string} appName - App identifier (for error context)
52
+ * @throws {Error} If a dangerous pattern is detected
53
+ */
54
+ _validateScript(scriptText, appName) {
55
+ for (const { pattern, label } of WuScriptExecutor.DANGEROUS_PATTERNS) {
56
+ if (pattern.test(scriptText)) {
57
+ const msg = `[ScriptExecutor] Blocked dangerous pattern in "${appName}": ${label}`;
58
+ logger.wuError(msg);
59
+ throw new Error(msg);
60
+ }
61
+ }
62
+ }
63
+
18
64
  /**
19
65
  * Execute a script string inside the proxy sandbox.
20
66
  *
@@ -31,6 +77,8 @@ export class WuScriptExecutor {
31
77
 
32
78
  if (!scriptText || !scriptText.trim()) return;
33
79
 
80
+ this._validateScript(scriptText, appName);
81
+
34
82
  const sourceComment = sourceUrl ? `\n//# sourceURL=wu-sandbox:///${appName}/${sourceUrl}\n` : '';
35
83
 
36
84
  let wrappedCode;
@@ -8,6 +8,15 @@
8
8
  * - API minimalista: get(), set(), on()
9
9
  */
10
10
 
11
+ /**
12
+ * @typedef {Object} WuStoreMetrics
13
+ * @property {number} reads - Total read operations
14
+ * @property {number} writes - Total write operations
15
+ * @property {number} notifications - Total notifications sent
16
+ * @property {number} bufferUtilization - Ring buffer utilization (0-1)
17
+ * @property {number} listenerCount - Active listener count
18
+ */
19
+
11
20
  export class WuStore {
12
21
  constructor(bufferSize = 256) {
13
22
  // Ring Buffer configuration
@@ -63,8 +72,9 @@ export class WuStore {
63
72
  set(path, value) {
64
73
  this.metrics.writes++;
65
74
 
66
- // Write to ring buffer (lock-free)
67
- const sequence = this.cursor++;
75
+ // Write to ring buffer (lock-free, wraps at buffer boundary)
76
+ const sequence = this.cursor;
77
+ this.cursor = (this.cursor + 1) % (this.bufferSize * this.bufferSize);
68
78
  const index = sequence & this.mask;
69
79
 
70
80
  // Reuse buffer slot (zero allocation)
@@ -151,7 +161,7 @@ export class WuStore {
151
161
  getMetrics() {
152
162
  return {
153
163
  ...this.metrics,
154
- bufferUtilization: (this.cursor % this.bufferSize) / this.bufferSize,
164
+ bufferUtilization: Math.min(1, this.cursor / this.bufferSize),
155
165
  listenerCount: this.listeners.size + this.patternListeners.size
156
166
  };
157
167
  }