wu-framework 2.1.2 → 2.5.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.
Files changed (72) hide show
  1. package/README.md +6 -1
  2. package/dist/adapters/alpine/index.d.ts +1 -1
  3. package/dist/adapters/angular/index.d.ts +1 -1
  4. package/dist/adapters/htmx/index.d.ts +1 -1
  5. package/dist/adapters/lit/index.d.ts +1 -1
  6. package/dist/adapters/lit/index.js +2 -2
  7. package/dist/adapters/lit/index.js.map +1 -1
  8. package/dist/adapters/preact/index.d.ts +1 -1
  9. package/dist/adapters/preact/index.js +1 -1
  10. package/dist/adapters/preact/index.js.map +1 -1
  11. package/dist/adapters/qwik/index.d.ts +3 -10
  12. package/dist/adapters/qwik/index.js +1 -1
  13. package/dist/adapters/qwik/index.js.map +1 -1
  14. package/dist/adapters/react/index.js +1 -1
  15. package/dist/adapters/react/index.js.map +1 -1
  16. package/dist/adapters/shared.d.ts +44 -0
  17. package/dist/adapters/shared.js +1 -1
  18. package/dist/adapters/shared.js.map +1 -1
  19. package/dist/adapters/solid/index.d.ts +1 -1
  20. package/dist/adapters/solid/index.js +1 -1
  21. package/dist/adapters/solid/index.js.map +1 -1
  22. package/dist/adapters/stencil/index.d.ts +1 -1
  23. package/dist/adapters/stimulus/index.d.ts +1 -1
  24. package/dist/adapters/svelte/index.d.ts +1 -1
  25. package/dist/adapters/svelte/index.js +1 -1
  26. package/dist/adapters/svelte/index.js.map +1 -1
  27. package/dist/adapters/vanilla/index.d.ts +1 -1
  28. package/dist/adapters/vanilla/index.js +1 -1
  29. package/dist/adapters/vanilla/index.js.map +1 -1
  30. package/dist/adapters/vue/index.js +1 -1
  31. package/dist/adapters/vue/index.js.map +1 -1
  32. package/dist/ai/wu-ai.js +1 -1
  33. package/dist/ai/wu-ai.js.map +1 -1
  34. package/dist/core/wu-devtools.js +2 -0
  35. package/dist/core/wu-devtools.js.map +1 -0
  36. package/dist/core/wu-html-parser.js +1 -1
  37. package/dist/core/wu-html-parser.js.map +1 -1
  38. package/dist/core/wu-iframe-sandbox.js +1 -1
  39. package/dist/core/wu-iframe-sandbox.js.map +1 -1
  40. package/dist/core/wu-loader.js +1 -1
  41. package/dist/core/wu-loader.js.map +1 -1
  42. package/dist/core/wu-logger.js +2 -0
  43. package/dist/core/wu-logger.js.map +1 -0
  44. package/dist/core/wu-mcp-bridge.js +1 -1
  45. package/dist/core/wu-mcp-bridge.js.map +1 -1
  46. package/dist/core/wu-script-executor.js +1 -1
  47. package/dist/core/wu-script-executor.js.map +1 -1
  48. package/dist/core/wu-store-sync.js +2 -0
  49. package/dist/core/wu-store-sync.js.map +1 -0
  50. package/dist/core/wu-timeline.js +2 -0
  51. package/dist/core/wu-timeline.js.map +1 -0
  52. package/dist/index.d.cts +739 -0
  53. package/dist/index.d.ts +295 -1
  54. package/dist/wu-ai-browser-primitives-CaUCk1Xl.js +2 -0
  55. package/dist/wu-ai-browser-primitives-CaUCk1Xl.js.map +1 -0
  56. package/dist/wu-framework.cjs +3 -0
  57. package/dist/wu-framework.cjs.map +1 -0
  58. package/dist/wu-framework.dev.js +1207 -275
  59. package/dist/wu-framework.dev.js.map +1 -1
  60. package/dist/wu-framework.esm.js +2 -2
  61. package/dist/wu-framework.esm.js.map +1 -1
  62. package/dist/wu-framework.umd.js +2 -2
  63. package/dist/wu-framework.umd.js.map +1 -1
  64. package/integrations/astro/WuApp.astro +16 -11
  65. package/integrations/astro/WuShell.astro +11 -3
  66. package/package.json +14 -6
  67. package/dist/wu-ai-browser-primitives-BDKXJlwc.js +0 -2
  68. package/dist/wu-ai-browser-primitives-BDKXJlwc.js.map +0 -1
  69. package/dist/wu-framework.cjs.js +0 -3
  70. package/dist/wu-framework.cjs.js.map +0 -1
  71. package/dist/wu-logger-fJfUHBGA.js +0 -2
  72. package/dist/wu-logger-fJfUHBGA.js.map +0 -1
@@ -1,150 +1,8 @@
1
- /*! wu-framework v2.1.1 | MIT License */
1
+ /*! wu-framework v2.5.0 | MIT License */
2
+ import { logger } from './core/wu-logger.js';
3
+ export { enableAllLogs, silenceAllLogs } from './core/wu-logger.js';
2
4
  export { WuLoader } from './core/wu-loader.js';
3
5
 
4
- /**
5
- * 📝 WU-LOGGER: Sistema de logging inteligente para entornos
6
- * Controla los logs automáticamente según el entorno
7
- */
8
-
9
- class WuLogger {
10
- constructor() {
11
- // Detectar entorno automáticamente
12
- this.isDevelopment = this.detectEnvironment();
13
- // En desarrollo: warn (menos ruido), en producción: error
14
- this.logLevel = this.isDevelopment ? 'warn' : 'error';
15
-
16
- this.levels = {
17
- debug: 0,
18
- info: 1,
19
- warn: 2,
20
- error: 3,
21
- silent: 4
22
- };
23
- }
24
-
25
- /**
26
- * Detectar si estamos en desarrollo
27
- */
28
- detectEnvironment() {
29
- // 1. Explicit flag takes priority
30
- if (typeof window !== 'undefined' && window.WU_DEBUG === true) return true;
31
- if (typeof window !== 'undefined' && window.WU_DEBUG === false) return false;
32
-
33
- // 2. NODE_ENV check (works in bundlers and Node)
34
- if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') return false;
35
- if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'development') return true;
36
-
37
- // 3. Browser heuristics (only if window exists)
38
- if (typeof window !== 'undefined' && window.location) {
39
- const hostname = window.location.hostname;
40
- if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]') return true;
41
-
42
- // URL param override
43
- try {
44
- if (new URLSearchParams(window.location.search).has('wu-debug')) return true;
45
- } catch {}
46
- }
47
-
48
- // 4. Default: assume production
49
- return false;
50
- }
51
-
52
- /**
53
- * Configurar nivel de logging
54
- */
55
- setLevel(level) {
56
- this.logLevel = level;
57
- return this;
58
- }
59
-
60
- /**
61
- * Habilitar/deshabilitar development mode
62
- */
63
- setDevelopment(isDev) {
64
- this.isDevelopment = isDev;
65
- this.logLevel = isDev ? 'debug' : 'error';
66
- return this;
67
- }
68
-
69
- /**
70
- * Verificar si debemos mostrar el log
71
- */
72
- shouldLog(level) {
73
- return this.levels[level] >= this.levels[this.logLevel];
74
- }
75
-
76
- /**
77
- * Logging methods
78
- */
79
- debug(...args) {
80
- if (this.shouldLog('debug')) {
81
- console.log(...args);
82
- }
83
- }
84
-
85
- info(...args) {
86
- if (this.shouldLog('info')) {
87
- console.info(...args);
88
- }
89
- }
90
-
91
- warn(...args) {
92
- if (this.shouldLog('warn')) {
93
- console.warn(...args);
94
- }
95
- }
96
-
97
- error(...args) {
98
- if (this.shouldLog('error')) {
99
- console.error(...args);
100
- }
101
- }
102
-
103
- /**
104
- * Logging con contexto Wu
105
- */
106
- wu(level, ...args) {
107
- if (this.shouldLog(level)) {
108
- const method = level === 'debug' ? 'log' : level;
109
- console[method]('[Wu]', ...args);
110
- }
111
- }
112
-
113
- /**
114
- * Helper methods específicos para Wu
115
- */
116
- wuDebug(...args) { this.wu('debug', ...args); }
117
- wuInfo(...args) { this.wu('info', ...args); }
118
- wuWarn(...args) { this.wu('warn', ...args); }
119
- wuError(...args) { this.wu('error', ...args); }
120
- }
121
-
122
- // Singleton instance
123
- const logger = new WuLogger();
124
-
125
- /**
126
- * 🔇 Silenciar todos los logs de Wu Framework
127
- * Útil en producción para eliminar todo el ruido
128
- */
129
- function silenceAllLogs() {
130
- logger.setLevel('silent');
131
- }
132
-
133
- /**
134
- * 🔊 Restaurar logs (nivel debug)
135
- */
136
- function enableAllLogs() {
137
- logger.setLevel('debug');
138
- }
139
-
140
- var wuLogger = /*#__PURE__*/Object.freeze({
141
- __proto__: null,
142
- WuLogger: WuLogger,
143
- enableAllLogs: enableAllLogs,
144
- logger: logger,
145
- silenceAllLogs: silenceAllLogs
146
- });
147
-
148
6
  /**
149
7
  * 🎨 WU-STYLE-BRIDGE: SHADOW DOM STYLE SHARING SYSTEM
150
8
  *
@@ -509,12 +367,20 @@ class WuStyleBridge {
509
367
  injectInlineStyle(shadowRoot, style) {
510
368
  // Verificar si ya existe
511
369
  const viteId = style.viteId;
370
+ const contentHash = viteId ? null : this._hashStyleContent(style.content);
512
371
  if (viteId) {
513
372
  const existing = shadowRoot.querySelector(`style[data-wu-vite-id="${viteId}"]`);
514
373
  if (existing) {
515
374
  logger.debug(`[WuStyleBridge] ⏭️ Inline style already exists: ${viteId}`);
516
375
  return;
517
376
  }
377
+ } else {
378
+ // Sin viteId (ej. CSS-in-JS runtime): dedup por hash de contenido
379
+ const existing = shadowRoot.querySelector(`style[data-wu-content-hash="${contentHash}"]`);
380
+ if (existing) {
381
+ logger.debug(`[WuStyleBridge] ⏭️ Inline style already exists (content hash: ${contentHash})`);
382
+ return;
383
+ }
518
384
  }
519
385
 
520
386
  // Crear nuevo style tag
@@ -524,6 +390,8 @@ class WuStyleBridge {
524
390
  styleTag.setAttribute('data-wu-library', style.library || 'unknown');
525
391
  if (viteId) {
526
392
  styleTag.setAttribute('data-wu-vite-id', viteId);
393
+ } else {
394
+ styleTag.setAttribute('data-wu-content-hash', contentHash);
527
395
  }
528
396
 
529
397
  // Insertar al principio del shadow root
@@ -532,6 +400,19 @@ class WuStyleBridge {
532
400
  logger.debug(`[WuStyleBridge] 📝 Injected inline style: ${style.library || viteId}`);
533
401
  }
534
402
 
403
+ /**
404
+ * #️⃣ HASH DE CONTENIDO: Hash rápido (djb2) para dedup de estilos inline sin viteId
405
+ * @param {string} content
406
+ * @returns {string}
407
+ */
408
+ _hashStyleContent(content) {
409
+ let hash = 5381;
410
+ for (let i = 0; i < content.length; i++) {
411
+ hash = ((hash << 5) + hash + content.charCodeAt(i)) | 0;
412
+ }
413
+ return `${content.length}_${(hash >>> 0).toString(36)}`;
414
+ }
415
+
535
416
  /**
536
417
  * 📋 INYECTAR ADOPTED STYLESHEET: Comparte stylesheet constructable
537
418
  * @param {ShadowRoot} shadowRoot
@@ -2524,6 +2405,10 @@ class WuStore {
2524
2405
  // Pattern listeners for wildcards
2525
2406
  this.patternListeners = new Map();
2526
2407
 
2408
+ // Low-level write taps (WuTimeline journaling). Lazily allocated — null
2409
+ // until something taps in, so the common path pays zero overhead.
2410
+ this._taps = null;
2411
+
2527
2412
  // Performance metrics
2528
2413
  this.metrics = {
2529
2414
  reads: 0,
@@ -2594,6 +2479,15 @@ class WuStore {
2594
2479
  // Update state synchronously
2595
2480
  this.updateState(path, value);
2596
2481
 
2482
+ // Synchronous low-level taps (journaling). Fired with the real sequence
2483
+ // number so WuTimeline's entries align with the ring-buffer order.
2484
+ if (this._taps && this._taps.size) {
2485
+ for (const tap of this._taps) {
2486
+ try { tap(sequence, path, value); }
2487
+ catch (error) { console.error('[WuStore] Tap error:', error); }
2488
+ }
2489
+ }
2490
+
2597
2491
  // Schedule async notifications (non-blocking)
2598
2492
  queueMicrotask(() => {
2599
2493
  this.notify(path, value);
@@ -2603,6 +2497,40 @@ class WuStore {
2603
2497
  return sequence;
2604
2498
  }
2605
2499
 
2500
+ /**
2501
+ * Register a low-level write tap. Unlike on(), taps fire synchronously
2502
+ * inside set() for EVERY write with the ring-buffer sequence number — the
2503
+ * substrate WuTimeline journals from. Returns an unsubscribe function.
2504
+ * @param {(sequence: number, path: string, value: *) => void} fn
2505
+ * @returns {Function} unsubscribe
2506
+ */
2507
+ tap(fn) {
2508
+ if (typeof fn !== 'function') return () => {};
2509
+ if (!this._taps) this._taps = new Set();
2510
+ this._taps.add(fn);
2511
+ return () => { this._taps?.delete(fn); };
2512
+ }
2513
+
2514
+ /**
2515
+ * Replace the whole state and re-notify listeners — the seek primitive for
2516
+ * WuTimeline. Does NOT touch the ring buffer, cursor, or taps, so a rewind
2517
+ * is invisible to journaling (no feedback loop). Notification is synchronous
2518
+ * (an explicit seek should reflect immediately), guarded per-listener.
2519
+ *
2520
+ * @param {Object} newState - The state to install
2521
+ * @param {Object} [opts]
2522
+ * @param {string[]} [opts.notifyPaths] - Paths to re-notify (default: top-level keys)
2523
+ */
2524
+ hydrate(newState, { notifyPaths } = {}) {
2525
+ this.state = (newState && typeof newState === 'object') ? newState : {};
2526
+ const paths = notifyPaths || Object.keys(this.state);
2527
+ for (const path of paths) {
2528
+ const value = this.get(path);
2529
+ this.notify(path, value);
2530
+ this.notifyPatterns(path, value);
2531
+ }
2532
+ }
2533
+
2606
2534
  /**
2607
2535
  * Subscribe to state changes
2608
2536
  * @param {string} pattern - Path or pattern (supports * wildcard)
@@ -2791,6 +2719,87 @@ class WuStore {
2791
2719
  return regex.test(path);
2792
2720
  }
2793
2721
 
2722
+ /**
2723
+ * Start real-time collaborative sync of this store across replicas — tabs
2724
+ * (BroadcastChannel), workers, or clients (WebSocket) — conflict-free via a
2725
+ * per-path Last-Writer-Wins CRDT. Writes made after sync() propagate to
2726
+ * peers; remote writes merge in and notify subscribers. Lazy-loads the sync
2727
+ * chunk (zero cost until called). See ROADMAP.md (#2).
2728
+ *
2729
+ * @param {Object} [opts]
2730
+ * @param {'broadcast'|WebSocket|string|{send,onMessage,close}} [opts.transport='broadcast']
2731
+ * @param {string} [opts.room='default'] - Channel name for the broadcast transport
2732
+ * @returns {{ stop: Function, status: Function, ready: Function, connected: boolean, instance: any }}
2733
+ *
2734
+ * @example
2735
+ * wu.store.sync({ transport: 'broadcast', room: 'cart' }); // cross-tab
2736
+ * wu.store.sync({ transport: 'wss://sync.example/doc-42' }); // cross-client
2737
+ */
2738
+ sync(opts = {}) {
2739
+ // One active sync per store. A second call returns the existing handle
2740
+ // (different opts are ignored — stop() first to reconfigure).
2741
+ if (this._syncHandle && !this._syncHandle._stopped) {
2742
+ console.warn('[WuStore] sync() already active — returning the existing handle. stop() it first to reconfigure.');
2743
+ return this._syncHandle;
2744
+ }
2745
+
2746
+ const store = this;
2747
+ let inst = null;
2748
+ let loading = null;
2749
+ let stopped = false;
2750
+ let error = null;
2751
+
2752
+ const ensure = () => {
2753
+ if (inst) return Promise.resolve(inst);
2754
+ if (!loading) {
2755
+ loading = import('./core/wu-store-sync.js')
2756
+ .then(({ WuStoreSync }) => {
2757
+ if (stopped) return null; // stop() raced ahead of the load
2758
+ const s = new WuStoreSync(store);
2759
+ s.connect(opts); // may throw (resolveTransport)
2760
+ inst = s; // assign ONLY after connect succeeds
2761
+ return inst;
2762
+ })
2763
+ .catch((err) => {
2764
+ // A failed connect must not wedge the one-sync-per-store guard.
2765
+ // Clear it so a later sync() can retry; resolve to null (honest
2766
+ // no-op) rather than rejecting (no unhandled-rejection surprise).
2767
+ error = err;
2768
+ if (store._syncHandle === handle) store._syncHandle = null;
2769
+ console.warn('[WuStore] sync() failed to connect:', err?.message || err);
2770
+ return null;
2771
+ });
2772
+ }
2773
+ return loading;
2774
+ };
2775
+
2776
+ const handle = {
2777
+ stop() {
2778
+ stopped = true; this._stopped = true;
2779
+ if (store._syncHandle === handle) store._syncHandle = null;
2780
+ if (inst) inst.stop();
2781
+ },
2782
+ status() {
2783
+ if (inst) return inst.status();
2784
+ // Stable shape across the load boundary (mirrors the loaded status()).
2785
+ return {
2786
+ connected: false, site: null, lamport: 0, peers: 0, tracked: 0,
2787
+ sent: 0, received: 0, applied: 0, ignored: 0, dropped: 0,
2788
+ loading: !stopped && !error, stopped,
2789
+ error: error ? String(error.message || error) : null,
2790
+ };
2791
+ },
2792
+ ready() { return ensure(); }, // await to guarantee the connection
2793
+ get connected() { return inst ? inst.status().connected : false; },
2794
+ get instance() { return inst; },
2795
+ _stopped: false,
2796
+ };
2797
+
2798
+ this._syncHandle = handle;
2799
+ ensure();
2800
+ return handle;
2801
+ }
2802
+
2794
2803
  /**
2795
2804
  * Clear all state and listeners
2796
2805
  */
@@ -2900,6 +2909,13 @@ class WuApp {
2900
2909
  await this._wu.init({
2901
2910
  apps: [{ name: this.name, url: this.url }]
2902
2911
  });
2912
+ } else if (!this._wu.apps.get(this.name)?.manifest) {
2913
+ // wu ya estaba inicializado: cargar el manifest de esta app
2914
+ await this._wu.registerApp({
2915
+ name: this.name,
2916
+ url: this.url,
2917
+ keepAlive: this.keepAlive
2918
+ });
2903
2919
  }
2904
2920
 
2905
2921
  // Montar usando wu-framework core
@@ -2923,7 +2939,7 @@ class WuApp {
2923
2939
  }
2924
2940
 
2925
2941
  await this._wu.unmount(this.name, options);
2926
- this._mounted = !this._wu.isHidden(this.name);
2942
+ this._mounted = false;
2927
2943
 
2928
2944
  return this
2929
2945
  }
@@ -3261,9 +3277,20 @@ class WuCache {
3261
3277
  if (this.config.persistent) {
3262
3278
  const stored = this.getFromStorage(key);
3263
3279
  if (stored) {
3264
- // Restaurar a memoria
3265
- this.memoryCache.set(key, stored);
3266
- this.accessOrder.set(key, Date.now());
3280
+ // Verificar TTL
3281
+ if (this.isExpired(stored)) {
3282
+ this.delete(key);
3283
+ this.stats.misses++;
3284
+ return null;
3285
+ }
3286
+
3287
+ // Restaurar a memoria (evicción solo en memoria: una lectura no
3288
+ // debe borrar copias persistentes de otras claves)
3289
+ if (this.ensureSpace(stored.size || 0, { demoteOnly: true }) !== false) {
3290
+ this.memoryCache.set(key, stored);
3291
+ this.accessOrder.set(key, Date.now());
3292
+ this.stats.size += stored.size || 0;
3293
+ }
3267
3294
  this.stats.hits++;
3268
3295
  return stored.value;
3269
3296
  }
@@ -3303,7 +3330,11 @@ class WuCache {
3303
3330
  return false;
3304
3331
  }
3305
3332
 
3306
- // Guardar en memoria
3333
+ // Guardar en memoria (descontar la entrada previa si se sobreescribe)
3334
+ const existing = this.memoryCache.get(key);
3335
+ if (existing) {
3336
+ this.stats.size -= existing.size;
3337
+ }
3307
3338
  this.memoryCache.set(key, entry);
3308
3339
  this.accessOrder.set(key, Date.now());
3309
3340
 
@@ -3390,7 +3421,7 @@ class WuCache {
3390
3421
  * 🎯 ENSURE SPACE: Asegurar espacio en cache (LRU eviction)
3391
3422
  * @param {number} neededSize - Tamaño necesario
3392
3423
  */
3393
- ensureSpace(neededSize) {
3424
+ ensureSpace(neededSize, { demoteOnly = false } = {}) {
3394
3425
  const maxSizeBytes = this.config.maxSize * 1024 * 1024;
3395
3426
 
3396
3427
  // 🛡️ FIX: Validar que el item no sea más grande que el máximo permitido
@@ -3429,7 +3460,16 @@ class WuCache {
3429
3460
 
3430
3461
  if (oldestKey) {
3431
3462
  logger.debug(`[WuCache] 🗑️ Evicting LRU entry: ${oldestKey}`);
3432
- this.delete(oldestKey);
3463
+ if (demoteOnly) {
3464
+ // Memory-only eviction: keep the persistent copy so the evicted
3465
+ // entry stays restorable (a read must never destroy other keys)
3466
+ const evicted = this.memoryCache.get(oldestKey);
3467
+ if (evicted) this.stats.size -= evicted.size;
3468
+ this.memoryCache.delete(oldestKey);
3469
+ this.accessOrder.delete(oldestKey);
3470
+ } else {
3471
+ this.delete(oldestKey);
3472
+ }
3433
3473
  this.stats.evictions++;
3434
3474
  } else {
3435
3475
  break;
@@ -3634,6 +3674,12 @@ class WuEventBus {
3634
3674
  this.listeners = new Map();
3635
3675
  this.history = [];
3636
3676
 
3677
+ // Low-level emit taps (WuTimeline journaling). Lazily allocated — null
3678
+ // until something taps in. Unlike on('*'), taps fire after history is
3679
+ // recorded and bypass wildcard-regex matching, so journaling is cheap and
3680
+ // never affected by an app's authorization scope.
3681
+ this._taps = null;
3682
+
3637
3683
  // Cache of compiled wildcard patterns. Avoids `new RegExp()` per emit per listener.
3638
3684
  this._wildcardCache = new Map();
3639
3685
 
@@ -3857,11 +3903,23 @@ class WuEventBus {
3857
3903
  verified: this.authorizedApps.has(appName)
3858
3904
  };
3859
3905
 
3860
- // Agregar a historial
3861
- if (this.config.enableReplay) {
3906
+ // Agregar a historial. `options.history === false` skips it — for
3907
+ // internal firehose events (e.g. WuTimeline's timeline:* lifecycle) that
3908
+ // would otherwise flood the bounded history and evict real app events from
3909
+ // it and from wu.inspect().events.recent. Listeners still receive them.
3910
+ if (this.config.enableReplay && options.history !== false) {
3862
3911
  this.addToHistory(event);
3863
3912
  }
3864
3913
 
3914
+ // Low-level taps (journaling) fire before listener dispatch, so a throwing
3915
+ // listener can't stop an event from being recorded.
3916
+ if (this._taps && this._taps.size) {
3917
+ for (const tap of this._taps) {
3918
+ try { tap(event); }
3919
+ catch (error) { console.error('[WuEventBus] Tap error:', error); }
3920
+ }
3921
+ }
3922
+
3865
3923
  // Log si está habilitado
3866
3924
  if (this.config.logEvents) {
3867
3925
  logger.debug(`[WuEventBus] 📢 ${eventName}`, data);
@@ -3888,6 +3946,21 @@ class WuEventBus {
3888
3946
  return true;
3889
3947
  }
3890
3948
 
3949
+ /**
3950
+ * 📼 TAP: Register a low-level emit tap. Fires for EVERY emitted event
3951
+ * (after history, before listeners), receiving the full event object.
3952
+ * Bypasses wildcard matching and authorization — it's a framework-internal
3953
+ * firehose for WuTimeline journaling. Returns an unsubscribe function.
3954
+ * @param {(event: WuEvent) => void} fn
3955
+ * @returns {Function} unsubscribe
3956
+ */
3957
+ tap(fn) {
3958
+ if (typeof fn !== 'function') return () => {};
3959
+ if (!this._taps) this._taps = new Set();
3960
+ this._taps.add(fn);
3961
+ return () => { this._taps?.delete(fn); };
3962
+ }
3963
+
3891
3964
  /**
3892
3965
  * 👂 ON: Suscribirse a evento
3893
3966
  */
@@ -3921,8 +3994,8 @@ class WuEventBus {
3921
3994
  */
3922
3995
  once(eventName, callback) {
3923
3996
  const wrappedCallback = (event) => {
3924
- callback(event);
3925
3997
  this.off(eventName, wrappedCallback);
3998
+ callback(event);
3926
3999
  };
3927
4000
  return this.on(eventName, wrappedCallback);
3928
4001
  }
@@ -4222,6 +4295,11 @@ class WuPerformance {
4222
4295
  const appMetrics = this.metrics.get(measurement.appName);
4223
4296
  appMetrics.measurements.push(measurement);
4224
4297
 
4298
+ // Mantener tamaño máximo por app
4299
+ if (appMetrics.measurements.length > this.config.maxMeasurements) {
4300
+ appMetrics.measurements.shift();
4301
+ }
4302
+
4225
4303
  // Calcular estadísticas
4226
4304
  this.calculateStats(measurement.appName);
4227
4305
  }
@@ -4699,11 +4777,13 @@ class WuPluginSystem {
4699
4777
  }
4700
4778
 
4701
4779
  // Registrar hooks del plugin con protección
4780
+ const registeredHooks = [];
4702
4781
  this.availableHooks.forEach(hookName => {
4703
4782
  if (typeof plugin[hookName] === 'function') {
4704
4783
  // Wrap el hook con timeout y try-catch
4705
4784
  const wrappedHook = this._wrapHook(plugin[hookName].bind(plugin), plugin.name, hookName);
4706
4785
  this.registerHook(hookName, wrappedHook);
4786
+ registeredHooks.push({ hookName, callback: wrappedHook });
4707
4787
  }
4708
4788
  });
4709
4789
 
@@ -4713,6 +4793,7 @@ class WuPluginSystem {
4713
4793
  options,
4714
4794
  permissions,
4715
4795
  sandboxedApi,
4796
+ registeredHooks,
4716
4797
  installedAt: Date.now()
4717
4798
  });
4718
4799
 
@@ -4759,7 +4840,9 @@ class WuPluginSystem {
4759
4840
  * 🎯 CALL HOOK
4760
4841
  */
4761
4842
  async callHook(hookName, context) {
4762
- const callbacks = this.hooks.get(hookName) || [];
4843
+ // Snapshot: uninstall() splices these arrays and would shift the
4844
+ // iterator past the next plugin's hook mid-dispatch
4845
+ const callbacks = [...(this.hooks.get(hookName) || [])];
4763
4846
 
4764
4847
  for (const callback of callbacks) {
4765
4848
  try {
@@ -4785,7 +4868,7 @@ class WuPluginSystem {
4785
4868
  return;
4786
4869
  }
4787
4870
 
4788
- const { plugin, sandboxedApi } = pluginData;
4871
+ const { plugin, sandboxedApi, registeredHooks } = pluginData;
4789
4872
 
4790
4873
  if (plugin.uninstall) {
4791
4874
  try {
@@ -4795,6 +4878,17 @@ class WuPluginSystem {
4795
4878
  }
4796
4879
  }
4797
4880
 
4881
+ // Remover los hooks registrados por el plugin
4882
+ if (registeredHooks) {
4883
+ for (const { hookName, callback } of registeredHooks) {
4884
+ const callbacks = this.hooks.get(hookName);
4885
+ if (callbacks) {
4886
+ const index = callbacks.indexOf(callback);
4887
+ if (index > -1) callbacks.splice(index, 1);
4888
+ }
4889
+ }
4890
+ }
4891
+
4798
4892
  this.plugins.delete(pluginName);
4799
4893
  logger.debug(`[WuPlugin] ✅ Plugin "${pluginName}" uninstalled`);
4800
4894
  }
@@ -5018,6 +5112,10 @@ class WuLoadingStrategy {
5018
5112
  async preload(apps) {
5019
5113
  const toPreload = apps.filter(app => {
5020
5114
  const strategy = this.strategies.get(app.strategy || 'lazy');
5115
+ if (!strategy) {
5116
+ logger.warn(`[WuStrategies] Strategy "${app.strategy}" not found, using lazy`);
5117
+ return false;
5118
+ }
5021
5119
  return strategy.shouldPreload;
5022
5120
  });
5023
5121
 
@@ -5561,6 +5659,8 @@ class WuLifecycleHooks {
5561
5659
  'afterLoad', // Después de cargar
5562
5660
  'beforeMount', // Antes de montar
5563
5661
  'afterMount', // Después de montar
5662
+ 'beforeUpdate', // Antes de empujar props vivas (wu.update)
5663
+ 'afterUpdate', // Después de empujar props vivas
5564
5664
  'beforeUnmount', // Antes de desmontar
5565
5665
  'afterUnmount', // Después de desmontar
5566
5666
  'beforeDestroy', // Antes de destruir framework
@@ -5661,10 +5761,9 @@ class WuLifecycleHooks {
5661
5761
 
5662
5762
  const hook = hooks[index];
5663
5763
  const startTime = Date.now();
5764
+ let nextCalled = false;
5664
5765
 
5665
5766
  try {
5666
- let nextCalled = false;
5667
-
5668
5767
  // Función next
5669
5768
  const next = async (modifiedContext) => {
5670
5769
  nextCalled = true;
@@ -5694,8 +5793,16 @@ class WuLifecycleHooks {
5694
5793
  } catch (error) {
5695
5794
  console.error(`[WuHooks] Error in hook "${hook.name}":`, error);
5696
5795
 
5697
- // Si hay error, pasar al siguiente hook
5698
- return await executeChain(index + 1);
5796
+ // Si hay error antes de llamar next(), pasar al siguiente hook.
5797
+ // Si next() ya se llamó, los hooks siguientes ya se ejecutaron.
5798
+ if (!nextCalled) {
5799
+ return await executeChain(index + 1);
5800
+ }
5801
+ }
5802
+
5803
+ // Propagar cancelación de hooks posteriores en la cadena
5804
+ if (cancelled) {
5805
+ return { cancelled: true };
5699
5806
  }
5700
5807
 
5701
5808
  return currentContext;
@@ -5996,15 +6103,16 @@ class WuPrefetch {
5996
6103
  const urls = await this._resolveAppUrls(appNames);
5997
6104
  if (urls.length === 0) return;
5998
6105
 
5999
- // Mark all as prefetched by name (prevents duplicate resolution)
6000
- urls.forEach(({ name }) => this.prefetched.add(name));
6001
-
6002
6106
  // Strategy 1: Speculation Rules API (Chrome 121+)
6107
+ // (_addSpeculationRules marks the names as prefetched itself)
6003
6108
  if (this.supportsSpeculationRules) {
6004
6109
  this._addSpeculationRules(urls, options.eagerness || 'moderate');
6005
6110
  return;
6006
6111
  }
6007
6112
 
6113
+ // Mark all as prefetched by name (prevents duplicate resolution)
6114
+ urls.forEach(({ name }) => this.prefetched.add(name));
6115
+
6008
6116
  // Strategy 2: <link rel="modulepreload"> for ES modules
6009
6117
  if (this.supportsModulePreload) {
6010
6118
  urls.forEach(({ url }) => this._injectModulePreload(url));
@@ -6819,6 +6927,413 @@ class WuOverrides {
6819
6927
  }
6820
6928
  }
6821
6929
 
6930
+ /**
6931
+ * WU-SEMVER: minimal semantic-version range matching (zero-dep).
6932
+ *
6933
+ * Enough for capability-contract negotiation (`wu.consume('cart', '^2.0')`),
6934
+ * not a full node-semver. Supports:
6935
+ * - exact: 1.2.3
6936
+ * - X-ranges: 1, 1.2, 1.x, *, "" (match anything)
6937
+ * - caret: ^1.2.3 (compatible within the leftmost non-zero)
6938
+ * - tilde: ~1.2.3 (patch-level within the minor)
6939
+ * - comparators: >= > <= < =
6940
+ * - AND: ">=1.0.0 <2.0.0" (space-separated)
6941
+ * - OR: "^1.0 || ^2.0"
6942
+ * Prerelease / build metadata is stripped (compared on major.minor.patch).
6943
+ */
6944
+
6945
+ function num(x) {
6946
+ const n = parseInt(x, 10);
6947
+ return Number.isNaN(n) ? 0 : n;
6948
+ }
6949
+
6950
+ /** Parse a version string to a [major, minor, patch] tuple. */
6951
+ function parseVersion(v) {
6952
+ const core = String(v).trim().replace(/^[v=\s]+/, '').split('+')[0].split('-')[0];
6953
+ const parts = core.split('.');
6954
+ return [num(parts[0]), num(parts[1]), num(parts[2])];
6955
+ }
6956
+
6957
+ /** Compare two versions (string or parsed tuple): -1 | 0 | 1. */
6958
+ function compareVersions(a, b) {
6959
+ const pa = Array.isArray(a) ? a : parseVersion(a);
6960
+ const pb = Array.isArray(b) ? b : parseVersion(b);
6961
+ for (let i = 0; i < 3; i++) {
6962
+ if (pa[i] !== pb[i]) return pa[i] < pb[i] ? -1 : 1;
6963
+ }
6964
+ return 0;
6965
+ }
6966
+
6967
+ function satisfiesComparator(version, raw) {
6968
+ const comp = raw.trim();
6969
+ if (comp === '' || comp === '*' || comp === 'x' || comp === 'X') return true;
6970
+
6971
+ // caret (^) / tilde (~)
6972
+ if (comp[0] === '^' || comp[0] === '~') {
6973
+ const caret = comp[0] === '^';
6974
+ const spec = comp.slice(1).trim();
6975
+ const base = parseVersion(spec);
6976
+ if (compareVersions(version, base) < 0) return false;
6977
+ // Count only CONCRETE leading components (x/X/* are unspecified), so the
6978
+ // upper bound for partial / zero ranges matches npm's X-range desugaring.
6979
+ const specifiedCount = spec
6980
+ .replace(/^[v=]+/, '').split('+')[0].split('-')[0]
6981
+ .split('.').filter((p) => p !== '' && p !== 'x' && p !== 'X' && p !== '*').length;
6982
+ let upper;
6983
+ if (caret) {
6984
+ // ^1.2.3 → <2; ^0.2.3 → <0.3; ^0.0.3 → <0.0.4; but ^0 → <1, ^0.0 → <0.1
6985
+ // (the leftmost non-zero rule needs how many components were written).
6986
+ if (base[0] > 0) upper = [base[0] + 1, 0, 0];
6987
+ else if (specifiedCount < 2) upper = [1, 0, 0];
6988
+ else if (base[1] > 0) upper = [0, base[1] + 1, 0];
6989
+ else if (specifiedCount < 3) upper = [0, 1, 0];
6990
+ else upper = [0, 0, base[2] + 1];
6991
+ } else {
6992
+ // ~1.2.3 / ~1.2 → <1.3.0 ; ~1 → <2.0.0
6993
+ upper = specifiedCount >= 2 ? [base[0], base[1] + 1, 0] : [base[0] + 1, 0, 0];
6994
+ }
6995
+ return compareVersions(parseVersion(version), upper) < 0;
6996
+ }
6997
+
6998
+ // explicit comparator
6999
+ const m = comp.match(/^(>=|<=|>|<|=)\s*(.+)$/);
7000
+ if (m) {
7001
+ const c = compareVersions(version, parseVersion(m[2]));
7002
+ switch (m[1]) {
7003
+ case '>': return c > 0;
7004
+ case '>=': return c >= 0;
7005
+ case '<': return c < 0;
7006
+ case '<=': return c <= 0;
7007
+ default: return c === 0; // '='
7008
+ }
7009
+ }
7010
+
7011
+ // bare version → X-range if partial / wildcarded, else exact
7012
+ const rawParts = comp.replace(/^[v=]+/, '').split('+')[0].split('-')[0].split('.');
7013
+ const wildcarded = rawParts.some((p) => p === 'x' || p === 'X' || p === '*');
7014
+ if (rawParts.length < 3 || wildcarded) {
7015
+ const vp = parseVersion(version);
7016
+ for (let i = 0; i < 3; i++) {
7017
+ const rp = rawParts[i];
7018
+ if (rp === undefined || rp === '' || rp === 'x' || rp === 'X' || rp === '*') continue;
7019
+ if (vp[i] !== num(rp)) return false;
7020
+ }
7021
+ return true;
7022
+ }
7023
+ return compareVersions(version, parseVersion(comp)) === 0;
7024
+ }
7025
+
7026
+ /**
7027
+ * Does `version` satisfy `range`?
7028
+ * @param {string} version - e.g. "2.3.0"
7029
+ * @param {string} range - e.g. "^2.0", "~1.2.3", ">=1.0.0 <2.0.0", "1.x || 2.x"
7030
+ * @returns {boolean}
7031
+ */
7032
+ function satisfies(version, range) {
7033
+ if (range === null || range === undefined || range === '' || range === '*') return true;
7034
+ return String(range).split('||').some((group) =>
7035
+ group.trim().split(/\s+/).filter(Boolean).every((comp) => satisfiesComparator(version, comp))
7036
+ );
7037
+ }
7038
+
7039
+ /**
7040
+ * WU-CONTRACTS: typed, versioned, runtime-verified capability contracts.
7041
+ *
7042
+ * Kills Module Federation's worst failure mode — silent shared-singleton
7043
+ * version skew that explodes at runtime as `undefined is not a function`.
7044
+ * Instead of sharing fragile singletons, independently-deployed micro-apps
7045
+ * declare what they OFFER and what they NEED, and the runtime negotiates and
7046
+ * verifies:
7047
+ *
7048
+ * // Provider (team Cart, deployed independently)
7049
+ * wu.provide('cart', { add(item){…}, get count(){…} }, { version: '2.3.0', app: 'cart' });
7050
+ *
7051
+ * // Consumer (team Header)
7052
+ * const cart = wu.consume('cart', '^2.0'); // honest failure if unsatisfied
7053
+ * cart.add(product);
7054
+ *
7055
+ * - Semver negotiation: `consume('cart','^2.0')` against the provider's declared
7056
+ * version; a mismatch fails LOUDLY ("cart@1.4 does not satisfy ^2.0") instead
7057
+ * of MF's silent breakage.
7058
+ * - Runtime shape verification: an impl that drifts from its declared interface
7059
+ * fails at provide() time, not at the consumer's call site.
7060
+ * - Lifecycle-aware: consume() returns a LIVE proxy that re-resolves on every
7061
+ * access, so a capability becoming unavailable (provider unmounts) yields a
7062
+ * clear "no provider for 'cart'" — never a stale reference. Capabilities are
7063
+ * auto-revoked when their providing app unmounts.
7064
+ * - Event-backed: `wu:capability:provided` / `wu:capability:revoked` let late
7065
+ * consumers `await wu.consume('cart','^2.0',{ wait:true })`.
7066
+ *
7067
+ * Eager (in the main bundle, like the store and event bus) — provide/consume
7068
+ * must work synchronously the moment an app mounts.
7069
+ */
7070
+
7071
+
7072
+ class WuContracts {
7073
+ /**
7074
+ * @param {object} core - The WuCore instance (for the event bus + lifecycle).
7075
+ */
7076
+ constructor(core) {
7077
+ this.core = core;
7078
+ this._providers = new Map(); // name -> { impl, version, app, shape, providedAt }
7079
+ this._waiters = new Map(); // name -> Set<{ range, resolve, reject, timer }>
7080
+ // NOTE: auto-revoke on app unmount is driven by WuCore._executeUnmount
7081
+ // calling revokeByApp() with the FRAMEWORK-VERIFIED app name — NOT by
7082
+ // listening to the app:unmounted bus event. The bus treats any 'app:'
7083
+ // event as system-trusted (no token), so a malicious micro-app could forge
7084
+ // app:unmounted with a rival's appName to revoke its capability. Driving it
7085
+ // from the core's own unmount path closes that spoofing vector.
7086
+ }
7087
+
7088
+ /**
7089
+ * Register a capability implementation.
7090
+ * @param {string} name
7091
+ * @param {object|Function} impl
7092
+ * @param {object} [opts]
7093
+ * @param {string} [opts.version='0.0.0'] - Semver the impl satisfies
7094
+ * @param {string[]|Object<string,string>} [opts.shape] - Required keys, or key→typeof
7095
+ * @param {string} [opts.app] - Providing app; the capability auto-revokes on its unmount
7096
+ * @returns {{ revoke: Function }}
7097
+ */
7098
+ provide(name, impl, opts = {}) {
7099
+ if (!name || typeof name !== 'string') {
7100
+ throw new Error('[WuContracts] provide() requires a capability name (string).');
7101
+ }
7102
+ if (impl === null || impl === undefined || (typeof impl !== 'object' && typeof impl !== 'function')) {
7103
+ throw new Error(`[WuContracts] provide('${name}') requires an implementation object or function.`);
7104
+ }
7105
+ const version = opts.version || '0.0.0';
7106
+ if (opts.shape) this._verifyShape(name, impl, opts.shape, version);
7107
+
7108
+ const prev = this._providers.get(name);
7109
+ if (prev && prev.version !== version) {
7110
+ logger.wuDebug?.(`[WuContracts] '${name}' re-provided: ${prev.version} → ${version}`);
7111
+ }
7112
+
7113
+ const record = { impl, version, app: opts.app || null, shape: opts.shape || null, providedAt: Date.now() };
7114
+ this._providers.set(name, record);
7115
+
7116
+ this._emit('wu:capability:provided', { name, version, app: record.app });
7117
+ this._resolveWaiters(name, record);
7118
+ logger.wuDebug?.(`[WuContracts] provided '${name}@${version}'`);
7119
+
7120
+ // Identity-scoped revoke: only revoke if THIS record is still the current
7121
+ // owner of the name. Prevents a stale handle (after a same-name re-provide
7122
+ // by another app) from killing the new owner's live capability.
7123
+ return {
7124
+ revoke: () => (this._providers.get(name) === record ? this.revoke(name) : false),
7125
+ };
7126
+ }
7127
+
7128
+ /**
7129
+ * Consume a capability. Returns a LIVE proxy (re-resolves on every access) —
7130
+ * never throws at consume() time, so you can consume before the provider
7131
+ * mounts; the first ACCESS throws a clear error if unsatisfied.
7132
+ *
7133
+ * With `{ wait: true }`, returns a Promise that resolves once a satisfying
7134
+ * provider appears (optionally bounded by `timeout` ms).
7135
+ *
7136
+ * @param {string} name
7137
+ * @param {string} [range='*'] - Semver range the provider must satisfy
7138
+ * @param {object} [opts]
7139
+ * @param {boolean} [opts.wait=false]
7140
+ * @param {number} [opts.timeout] - ms; rejects the wait promise if exceeded
7141
+ * @returns {Proxy|Promise<Proxy>}
7142
+ */
7143
+ consume(name, range = '*', opts = {}) {
7144
+ if (!opts.wait) return this._proxy(name, range);
7145
+
7146
+ const cur = this._providers.get(name);
7147
+ if (cur && satisfies(cur.version, range)) return Promise.resolve(this._proxy(name, range));
7148
+
7149
+ return new Promise((resolve, reject) => {
7150
+ const waiter = {
7151
+ range,
7152
+ resolve: () => resolve(this._proxy(name, range)),
7153
+ reject,
7154
+ timer: null,
7155
+ };
7156
+ if (opts.timeout) {
7157
+ waiter.timer = setTimeout(() => {
7158
+ this._removeWaiter(name, waiter);
7159
+ reject(new Error(`[WuContracts] timed out after ${opts.timeout}ms waiting for '${name}' satisfying '${range}'.`));
7160
+ }, opts.timeout);
7161
+ }
7162
+ this._addWaiter(name, waiter);
7163
+ });
7164
+ }
7165
+
7166
+ /** Is a satisfying provider currently registered? (never throws) */
7167
+ has(name, range = '*') {
7168
+ const rec = this._providers.get(name);
7169
+ return !!rec && satisfies(rec.version, range);
7170
+ }
7171
+
7172
+ /** Revoke a capability (e.g. on teardown). */
7173
+ revoke(name) {
7174
+ if (this._providers.delete(name)) {
7175
+ this._emit('wu:capability:revoked', { name });
7176
+ logger.wuDebug?.(`[WuContracts] revoked '${name}'`);
7177
+ return true;
7178
+ }
7179
+ return false;
7180
+ }
7181
+
7182
+ /**
7183
+ * Revoke every capability provided by `app`. Called by WuCore on a real
7184
+ * unmount with the framework-verified app name (never from a bus event).
7185
+ * @param {string} app
7186
+ */
7187
+ revokeByApp(app) {
7188
+ if (!app) return;
7189
+ for (const [name, rec] of [...this._providers]) {
7190
+ if (rec.app === app) this.revoke(name);
7191
+ }
7192
+ }
7193
+
7194
+ /** Snapshot of registered capabilities (for wu.inspect() / devtools). */
7195
+ list() {
7196
+ const out = [];
7197
+ for (const [name, rec] of this._providers) {
7198
+ out.push({ name, version: rec.version, app: rec.app });
7199
+ }
7200
+ return out;
7201
+ }
7202
+
7203
+ cleanup() {
7204
+ for (const set of this._waiters.values()) {
7205
+ for (const w of set) {
7206
+ if (w.timer) clearTimeout(w.timer);
7207
+ try { w.reject(new Error('[WuContracts] framework destroyed')); } catch { /* noop */ }
7208
+ }
7209
+ }
7210
+ this._waiters.clear();
7211
+ this._providers.clear();
7212
+ }
7213
+
7214
+ /* ─────────────────────────────── private ────────────────────────── */
7215
+
7216
+ _proxy(name, range) {
7217
+ const contracts = this;
7218
+ const resolve = () => {
7219
+ const rec = contracts._providers.get(name);
7220
+ if (!rec) {
7221
+ throw new Error(`[WuContracts] no provider for capability '${name}'. ` +
7222
+ `Did the providing app mount and call wu.provide('${name}', …)?`);
7223
+ }
7224
+ if (!satisfies(rec.version, range)) {
7225
+ throw new Error(`[WuContracts] capability '${name}@${rec.version}' does not satisfy '${range}'.`);
7226
+ }
7227
+ return rec;
7228
+ };
7229
+
7230
+ // Memoize bound methods so `proxy.fn === proxy.fn` (stable identity needed
7231
+ // for add/removeListener round-trips, Set/Map keys, React deps). Keyed by
7232
+ // the unbound source fn — a swapped provider yields a new fn → new binding,
7233
+ // so re-resolution stays live.
7234
+ const boundCache = new WeakMap();
7235
+ const bind = (impl, fn) => {
7236
+ let b = boundCache.get(fn);
7237
+ if (!b) { b = fn.bind(impl); boundCache.set(fn, b); }
7238
+ return b;
7239
+ };
7240
+
7241
+ return new Proxy(Object.create(null), {
7242
+ get(_, prop) {
7243
+ if (prop === '__wuCapability') return name;
7244
+ if (prop === '__wuRange') return range;
7245
+ if (prop === '__wuAvailable') return contracts.has(name, range);
7246
+ // The proxy is NEVER thenable. A capability method literally named
7247
+ // `then` is unreachable via the proxy (documented) — far safer than
7248
+ // silently assimilating into a Promise on await/Promise.resolve.
7249
+ if (prop === 'then') return undefined;
7250
+ // Don't throw on Symbol probes (iterators, toPrimitive, …).
7251
+ if (typeof prop === 'symbol') {
7252
+ if (!contracts.has(name, range)) return undefined;
7253
+ const rec = contracts._providers.get(name);
7254
+ const v = rec.impl[prop];
7255
+ return typeof v === 'function' ? bind(rec.impl, v) : v;
7256
+ }
7257
+ const rec = resolve();
7258
+ const val = rec.impl[prop];
7259
+ return typeof val === 'function' ? bind(rec.impl, val) : val;
7260
+ },
7261
+ set(_, prop) {
7262
+ // Consumed capabilities are READ-ONLY — a consumer writing through the
7263
+ // proxy would mutate the provider's shared impl (and could repoint its
7264
+ // prototype via __proto__). Honest no-op + warn.
7265
+ logger.wuWarn?.(`[WuContracts] ignored write to '${String(prop)}' on consumed capability '${name}': consumed capabilities are read-only.`);
7266
+ return true;
7267
+ },
7268
+ has(_, prop) {
7269
+ const rec = contracts._providers.get(name);
7270
+ return rec ? (prop in rec.impl) : false;
7271
+ },
7272
+ });
7273
+ }
7274
+
7275
+ _verifyShape(name, impl, shape, version) {
7276
+ const problems = [];
7277
+ if (Array.isArray(shape)) {
7278
+ for (const key of shape) if (!WuContracts._provides(impl, key)) problems.push(`${key} (missing)`);
7279
+ } else if (shape && typeof shape === 'object') {
7280
+ for (const [key, type] of Object.entries(shape)) {
7281
+ if (!WuContracts._provides(impl, key)) problems.push(`${key} (missing)`);
7282
+ else if (type && typeof impl[key] !== type) problems.push(`${key} (expected ${type}, got ${typeof impl[key]})`);
7283
+ }
7284
+ }
7285
+ if (problems.length) {
7286
+ throw new Error(`[WuContracts] provide('${name}@${version}') violates its declared shape — ${problems.join(', ')}.`);
7287
+ }
7288
+ }
7289
+
7290
+ /**
7291
+ * Is `key` GENUINELY provided by impl? Walks own props + the impl's own
7292
+ * prototype chain (so class-instance methods count) but stops before
7293
+ * Object/Function.prototype — otherwise inherited toString/valueOf/hasOwnProperty
7294
+ * would let a hollow `{}` satisfy any contract demanding those names.
7295
+ * @private
7296
+ */
7297
+ static _provides(impl, key) {
7298
+ let obj = impl;
7299
+ while (obj && obj !== Object.prototype && obj !== Function.prototype) {
7300
+ if (Object.prototype.hasOwnProperty.call(obj, key)) return true;
7301
+ obj = Object.getPrototypeOf(obj);
7302
+ }
7303
+ return false;
7304
+ }
7305
+
7306
+ _addWaiter(name, waiter) {
7307
+ if (!this._waiters.has(name)) this._waiters.set(name, new Set());
7308
+ this._waiters.get(name).add(waiter);
7309
+ }
7310
+
7311
+ _removeWaiter(name, waiter) {
7312
+ const set = this._waiters.get(name);
7313
+ if (set) { set.delete(waiter); if (!set.size) this._waiters.delete(name); }
7314
+ }
7315
+
7316
+ _resolveWaiters(name, rec) {
7317
+ const set = this._waiters.get(name);
7318
+ if (!set) return;
7319
+ for (const w of [...set]) {
7320
+ if (satisfies(rec.version, w.range)) {
7321
+ if (w.timer) clearTimeout(w.timer);
7322
+ set.delete(w);
7323
+ w.resolve();
7324
+ }
7325
+ }
7326
+ if (!set.size) this._waiters.delete(name);
7327
+ }
7328
+
7329
+ _emit(event, data) {
7330
+ try {
7331
+ // `wu:` events are system-trusted by the bus (no token needed).
7332
+ this.core.eventBus?.emit(event, data, { appName: 'wu-core' });
7333
+ } catch { /* bus may be torn down */ }
7334
+ }
7335
+ }
7336
+
6822
7337
  /**
6823
7338
  * WU-FRAMEWORK: UNIVERSAL MICROFRONTENDS
6824
7339
  * Motor principal agnostico - Funciona con cualquier framework
@@ -6834,7 +7349,8 @@ class WuCore {
6834
7349
  this.mounted = new Map(); // Apps montadas
6835
7350
  this.hidden = new Map(); // Keep-alive hidden apps
6836
7351
  this._pendingUnmounts = new Map(); // Deferred unmount timers (StrictMode compat)
6837
- this._mountingPromises = new Map(); // In-flight mount dedup
7352
+ this._mountingPromises = new Map(); // In-flight mount dedup: appName -> { promise, containerSelector }
7353
+ this._unmountingPromises = new Map(); // In-flight deferred teardowns: appName -> Promise
6838
7354
  this._defineWaiters = new Map(); // Promise waiters for wu.define(): appName -> { resolve, timer, promise }
6839
7355
  this._resolvedPaths = new Map(); // Cached module URL: cacheKey -> resolved path
6840
7356
  // Reference counting for mount/unmount. Survives multi-cycle StrictMode +
@@ -6856,6 +7372,9 @@ class WuCore {
6856
7372
  // Loader for wu.use() — same lazy pattern. Only instantiated when a
6857
7373
  // caller actually requests a shared component. See _ensureLoader().
6858
7374
  this.loader = null;
7375
+ // Time-travel recorder — lazy chunk, instantiated on first wu.timeline.*
7376
+ // access. See _ensureTimeline().
7377
+ this._timeline = null;
6859
7378
 
6860
7379
  // Sistemas esenciales
6861
7380
  // Cache: localStorage backing has a real-world ceiling of ~5MB per origin.
@@ -6873,6 +7392,10 @@ class WuCore {
6873
7392
  this.hooks = new WuLifecycleHooks(this);
6874
7393
  this.prefetcher = new WuPrefetch(this);
6875
7394
  this.overrides = new WuOverrides();
7395
+ // Capability contracts — typed/versioned provide/consume between apps.
7396
+ // Eager (like store/eventBus): provide/consume must work synchronously
7397
+ // the moment an app mounts. Needs eventBus, which is created above.
7398
+ this.contracts = new WuContracts(this);
6876
7399
 
6877
7400
  // Estado
6878
7401
  this.isInitialized = false;
@@ -7026,43 +7549,174 @@ class WuCore {
7026
7549
  logger.wuDebug(`${appName} deferred unmount cancelled by remount`);
7027
7550
  }
7028
7551
 
7029
- // Already mounted in same container no-op (refs already incremented above)
7030
- if (this.mounted.has(appName)) {
7031
- const existing = this.mounted.get(appName);
7032
- if (existing.containerSelector === containerSelector) {
7033
- logger.wuDebug(`${appName} already mounted in ${containerSelector}`);
7034
- return;
7552
+ // Failed/cancelled mounts must release their ref, otherwise future
7553
+ // unmounts are skipped forever ("refs still active").
7554
+ let refReleased = false;
7555
+ const releaseRef = () => {
7556
+ if (refReleased) return;
7557
+ refReleased = true;
7558
+ this._releaseMountRef(appName);
7559
+ };
7560
+ // Teardowns (force unmount / deferred teardown) delete the whole ref
7561
+ // entry, taking this caller's +1 with it — re-establish it so the
7562
+ // remounted instance survives until its own unmount.
7563
+ const reclaimRef = () => {
7564
+ if (!refReleased && (this._mountRefs.get(appName) || 0) === 0) {
7565
+ this._mountRefs.set(appName, 1);
7566
+ }
7567
+ };
7568
+
7569
+ try {
7570
+ // Wait for any in-flight deferred teardown before evaluating mounted
7571
+ // state — otherwise we'd report "already mounted" while the teardown
7572
+ // finishes underneath and leaves the container empty. The wait is
7573
+ // bounded: a beforeUnmount/afterUnmount hook that mounts this same
7574
+ // app would otherwise form a circular teardown→hook→mount() wait.
7575
+ if (this._unmountingPromises.has(appName)) {
7576
+ await Promise.race([
7577
+ this._unmountingPromises.get(appName),
7578
+ new Promise((resolve) => setTimeout(resolve, 5000)),
7579
+ ]);
7580
+ reclaimRef();
7035
7581
  }
7582
+
7583
+ // Already mounted in same container → no-op (refs already incremented above)
7584
+ if (this.mounted.has(appName)) {
7585
+ const existing = this.mounted.get(appName);
7586
+ if (existing.containerSelector === containerSelector) {
7587
+ logger.wuDebug(`${appName} already mounted in ${containerSelector}`);
7588
+ return;
7589
+ }
7590
+ // Different container → destroy the old instance, then remount normally
7591
+ await this.unmount(appName, { force: true });
7592
+ reclaimRef();
7593
+ }
7594
+
7595
+ // Deduplicate concurrent mounts (StrictMode fires effect twice)
7596
+ const inflight = this._mountingPromises.get(appName);
7597
+ if (inflight) {
7598
+ if (inflight.containerSelector === containerSelector) {
7599
+ logger.wuDebug(`${appName} mount already in progress, deduplicating`);
7600
+ return await inflight.promise;
7601
+ }
7602
+ // Different container → wait for the in-flight mount to settle,
7603
+ // then remount normally (the recursive call manages its own ref)
7604
+ releaseRef();
7605
+ await inflight.promise.catch(() => {});
7606
+ return await this.mount(appName, containerSelector);
7607
+ }
7608
+
7609
+ // Check if app is in keep-alive (hidden) state
7610
+ const hiddenEntry = this.hidden.get(appName);
7611
+ if (hiddenEntry) {
7612
+ if (hiddenEntry.containerSelector === containerSelector) {
7613
+ // Same container → instant show (no reload)
7614
+ return await this.show(appName);
7615
+ }
7616
+ // Different container → destroy hidden state, remount normally
7617
+ await this._destroyHidden(appName);
7618
+ }
7619
+
7620
+ // Track mount promise for deduplication
7621
+ const mountPromise = this.mountWithRecovery(appName, containerSelector, 0);
7622
+ this._mountingPromises.set(appName, { promise: mountPromise, containerSelector });
7623
+
7624
+ try {
7625
+ return await mountPromise;
7626
+ } finally {
7627
+ this._mountingPromises.delete(appName);
7628
+ }
7629
+ } catch (error) {
7630
+ releaseRef();
7631
+ throw error;
7036
7632
  }
7633
+ }
7037
7634
 
7038
- // Deduplicate concurrent mounts (StrictMode fires effect twice)
7039
- if (this._mountingPromises.has(appName)) {
7040
- logger.wuDebug(`${appName} mount already in progress, deduplicating`);
7041
- return await this._mountingPromises.get(appName);
7635
+ /**
7636
+ * Push new props into an already-mounted micro-app WITHOUT remounting —
7637
+ * the live-props channel. Calls the app's optional `update(container, props)`
7638
+ * lifecycle slot (adapters built on createWuAdapter expose it when their
7639
+ * framework can re-render in place).
7640
+ *
7641
+ * Honest no-op semantics: returns false (and warns once) when the app is not
7642
+ * mounted or its adapter did not advertise an `update` slot — never throws,
7643
+ * never silently pretends to have updated.
7644
+ *
7645
+ * @param {string} appName - Nombre de la app montada
7646
+ * @param {Object} props - Props a fusionar/empujar a la app
7647
+ * @returns {Promise<boolean>} true si la app recibió las props
7648
+ */
7649
+ async update(appName, props = {}) {
7650
+ // Wait out any in-flight deferred teardown so we don't update an app that
7651
+ // is about to disappear (mirrors mount()'s teardown coordination).
7652
+ if (this._unmountingPromises.has(appName)) {
7653
+ await Promise.race([
7654
+ this._unmountingPromises.get(appName),
7655
+ new Promise((resolve) => setTimeout(resolve, 5000)),
7656
+ ]);
7657
+ }
7658
+
7659
+ // Re-validate AFTER the race: if the timeout won (teardown >5s) the mount
7660
+ // record may still be present mid-unmount — its container/lifecycle are
7661
+ // already torn down, so don't update it.
7662
+ if (this._unmountingPromises.has(appName)) {
7663
+ logger.wuWarn(`[Wu] update('${appName}') ignored: app is being unmounted`);
7664
+ return false;
7042
7665
  }
7043
7666
 
7044
- // Check if app is in keep-alive (hidden) state
7045
- const hiddenEntry = this.hidden.get(appName);
7046
- if (hiddenEntry) {
7047
- if (hiddenEntry.containerSelector === containerSelector) {
7048
- // Same container → instant show (no reload)
7049
- return await this.show(appName);
7050
- }
7051
- // Different container → destroy hidden state, remount normally
7052
- await this._destroyHidden(appName);
7667
+ const mounted = this.mounted.get(appName) || this.hidden.get(appName);
7668
+ if (!mounted) {
7669
+ logger.wuWarn(`[Wu] update('${appName}') ignored: app is not mounted`);
7670
+ return false;
7053
7671
  }
7054
7672
 
7055
- // Track mount promise for deduplication
7056
- const mountPromise = this.mountWithRecovery(appName, containerSelector, 0);
7057
- this._mountingPromises.set(appName, mountPromise);
7673
+ const lifecycle = mounted.lifecycle;
7674
+ if (!lifecycle || typeof lifecycle.update !== 'function') {
7675
+ logger.wuWarn(
7676
+ `[Wu] update('${appName}') is a no-op: this app's adapter does not support ` +
7677
+ `live props (no update() slot). Re-mount to change props, or use the ` +
7678
+ `event bus / store for cross-app state.`
7679
+ );
7680
+ return false;
7681
+ }
7058
7682
 
7059
7683
  try {
7060
- return await mountPromise;
7061
- } finally {
7062
- this._mountingPromises.delete(appName);
7684
+ // A beforeUpdate hook may veto the push, like every other before* phase.
7685
+ const before = await this.hooks.execute('beforeUpdate', { appName, props, mounted });
7686
+ if (before.cancelled) {
7687
+ logger.wuWarn(`[Wu] update('${appName}') cancelled by beforeUpdate hook`);
7688
+ return false;
7689
+ }
7690
+ await lifecycle.update(mounted.container, props);
7691
+ // Retain the latest props on the mount record for inspection/devtools.
7692
+ mounted.props = { ...(mounted.props || {}), ...props };
7693
+ await this.hooks.execute('afterUpdate', { appName, props });
7694
+ this.eventBus.emit('app:updated', { appName, props }, { appName });
7695
+ logger.wuDebug(`${appName} props updated`);
7696
+ return true;
7697
+ } catch (error) {
7698
+ logger.wuError(`[Wu] update('${appName}') failed:`, error);
7699
+ this.errorBoundary.handle(error, { appName, phase: 'update' });
7700
+ return false;
7063
7701
  }
7064
7702
  }
7065
7703
 
7704
+ /**
7705
+ * Release one mount reference. Used when a mount fails or is cancelled,
7706
+ * so the ref count stays balanced with successful mounts.
7707
+ * @private
7708
+ */
7709
+ _releaseMountRef(appName) {
7710
+ const prev = this._mountRefs.get(appName) || 0;
7711
+ const next = prev - 1;
7712
+ if (next > 0) {
7713
+ this._mountRefs.set(appName, next);
7714
+ } else {
7715
+ this._mountRefs.delete(appName);
7716
+ }
7717
+ logger.wuDebug(`${appName} mount ref released: ${prev} → ${Math.max(0, next)}`);
7718
+ }
7719
+
7066
7720
  /**
7067
7721
  * Mount with recovery: self-healing app mounting
7068
7722
  */
@@ -7079,6 +7733,7 @@ class WuCore {
7079
7733
  const beforeLoadResult = await this.hooks.execute('beforeLoad', { appName, containerSelector, attempt });
7080
7734
  if (beforeLoadResult.cancelled) {
7081
7735
  logger.wuWarn('Mount cancelled by beforeLoad hook');
7736
+ this._releaseMountRef(appName);
7082
7737
  return;
7083
7738
  }
7084
7739
 
@@ -7086,6 +7741,7 @@ class WuCore {
7086
7741
  const pluginBeforeMount = await this.pluginSystem.callHook('beforeMount', { appName, containerSelector });
7087
7742
  if (pluginBeforeMount === false) {
7088
7743
  logger.wuWarn('Mount cancelled by plugin beforeMount hook');
7744
+ this._releaseMountRef(appName);
7089
7745
  return;
7090
7746
  }
7091
7747
 
@@ -7127,6 +7783,12 @@ class WuCore {
7127
7783
  const beforeMountResult = await this.hooks.execute('beforeMount', { appName, containerSelector, sandbox, lifecycle });
7128
7784
  if (beforeMountResult.cancelled) {
7129
7785
  logger.wuWarn('Mount cancelled by beforeMount hook');
7786
+ if (sandbox.iframeSandbox) {
7787
+ sandbox.iframeSandbox.destroy();
7788
+ sandbox.iframeSandbox = null;
7789
+ }
7790
+ this.sandbox.cleanup(sandbox);
7791
+ this._releaseMountRef(appName);
7130
7792
  return;
7131
7793
  }
7132
7794
 
@@ -7173,8 +7835,12 @@ class WuCore {
7173
7835
  try {
7174
7836
  if (this.sandbox && this.sandbox.sandboxes && this.sandbox.sandboxes.has(appName)) {
7175
7837
  const sb = this.sandbox.sandboxes.get(appName);
7176
- if (sb && sb.proxySandbox) {
7177
- sb.proxySandbox.deactivate();
7838
+ if (sb && sb.iframeSandbox) {
7839
+ sb.iframeSandbox.destroy();
7840
+ sb.iframeSandbox = null;
7841
+ }
7842
+ if (sb && sb.jsSandbox && sb.jsSandbox.isActive()) {
7843
+ sb.jsSandbox.deactivate();
7178
7844
  }
7179
7845
  this.sandbox.sandboxes.delete(appName);
7180
7846
  logger.wuDebug(`Sandbox cleaned up after mount failure for ${appName}`);
@@ -7191,18 +7857,20 @@ class WuCore {
7191
7857
  container: containerSelector
7192
7858
  });
7193
7859
 
7194
- // Si el error boundary recupero el error, no necesitamos reintentar
7195
- if (errorResult.recovered) {
7196
- logger.wuDebug('Error recovered by error boundary');
7197
- return;
7198
- }
7199
-
7200
- // Recovery protocol
7201
- if (attempt < maxAttempts - 1 && errorResult.action === 'retry') {
7860
+ // Recovery protocol: handlers that ask for a retry get actual retries
7861
+ const wantsRetry = errorResult.action === 'retry' ||
7862
+ errorResult.action === 'retry-with-longer-timeout';
7863
+ if (wantsRetry && attempt < maxAttempts - 1) {
7202
7864
  logger.wuDebug('Initiating recovery protocol...');
7203
7865
 
7204
- // Clean app state
7205
- await this.appStateCleanup(appName, containerSelector);
7866
+ // Clean app state (keeping the wu.define() registration: the module
7867
+ // is cached by the browser and won't re-register on re-import)
7868
+ await this.appStateCleanup(appName, containerSelector, { preserveDefinition: true });
7869
+ // The cleanup's force-unmount wipes the ref table — restore the
7870
+ // caller's ref so the retried mount stays balanced with its unmount.
7871
+ if ((this._mountRefs.get(appName) || 0) === 0) {
7872
+ this._mountRefs.set(appName, 1);
7873
+ }
7206
7874
 
7207
7875
  // Temporal stabilization
7208
7876
  await new Promise(resolve => setTimeout(resolve, 1000 * (attempt + 1)));
@@ -7211,6 +7879,14 @@ class WuCore {
7211
7879
  return await this.mountWithRecovery(appName, containerSelector, attempt + 1);
7212
7880
  }
7213
7881
 
7882
+ // Recovered without a retry request (custom handler resolved it).
7883
+ // Nothing was mounted by us, so the ref must be released.
7884
+ if (errorResult.recovered && !wantsRetry) {
7885
+ logger.wuDebug('Error recovered by error boundary');
7886
+ this._releaseMountRef(appName);
7887
+ return;
7888
+ }
7889
+
7214
7890
  // Call plugin error hooks
7215
7891
  await this.pluginSystem.callHook('onError', { phase: 'mount', error, appName });
7216
7892
 
@@ -7221,8 +7897,10 @@ class WuCore {
7221
7897
 
7222
7898
  /**
7223
7899
  * App state cleanup: Enhanced container cleanup with framework protection
7900
+ * `preserveDefinition` keeps the wu.define() registration: a cached ES
7901
+ * module won't re-execute on re-import, so a retry could never restore it.
7224
7902
  */
7225
- async appStateCleanup(appName, containerSelector) {
7903
+ async appStateCleanup(appName, containerSelector, { preserveDefinition = false } = {}) {
7226
7904
  try {
7227
7905
  logger.wuDebug(`Starting app state cleanup for ${appName}...`);
7228
7906
 
@@ -7280,7 +7958,9 @@ class WuCore {
7280
7958
  }
7281
7959
 
7282
7960
  // Reset definition state
7283
- this.definitions.delete(appName);
7961
+ if (!preserveDefinition) {
7962
+ this.definitions.delete(appName);
7963
+ }
7284
7964
 
7285
7965
  // Clear sandbox registry
7286
7966
  if (this.sandbox && this.sandbox.sandboxes) {
@@ -7548,7 +8228,15 @@ class WuCore {
7548
8228
  }
7549
8229
 
7550
8230
  // Wait for wu.define()
7551
- await this._waitForDefine(app.name, 'strict');
8231
+ try {
8232
+ await this._waitForDefine(app.name, 'strict');
8233
+ } catch (defineError) {
8234
+ // Module imported but never called wu.define() — destroy the iframe
8235
+ // so each failed strict mount doesn't leak a hidden window.
8236
+ iframeSandbox.destroy();
8237
+ sandbox.iframeSandbox = null;
8238
+ throw defineError;
8239
+ }
7552
8240
 
7553
8241
  logger.wuDebug(`[strict] ${app.name} loaded and registered via iframe`);
7554
8242
  }
@@ -7905,18 +8593,25 @@ class WuCore {
7905
8593
  clearTimeout(this._pendingUnmounts.get(appName));
7906
8594
  }
7907
8595
 
7908
- this._pendingUnmounts.set(appName, setTimeout(async () => {
8596
+ this._pendingUnmounts.set(appName, setTimeout(() => {
7909
8597
  this._pendingUnmounts.delete(appName);
7910
8598
  // Re-check refs at fire time — caller may have re-mounted during grace
7911
8599
  if ((this._mountRefs.get(appName) || 0) > 0) return;
7912
8600
  // Re-verify: only unmount if the same mount entry is still current
7913
8601
  if (this.mounted.has(appName) && this.mounted.get(appName) === mounted) {
7914
- try {
7915
- await this._executeUnmount(appName, mounted);
7916
- this._mountRefs.delete(appName);
7917
- } catch (error) {
7918
- logger.wuError(`Deferred unmount failed for ${appName}:`, error);
7919
- }
8602
+ // Track the in-flight teardown synchronously so a concurrent mount()
8603
+ // awaits it instead of seeing a half-unmounted app as "already mounted"
8604
+ const teardown = (async () => {
8605
+ try {
8606
+ await this._executeUnmount(appName, mounted);
8607
+ this._mountRefs.delete(appName);
8608
+ } catch (error) {
8609
+ logger.wuError(`Deferred unmount failed for ${appName}:`, error);
8610
+ } finally {
8611
+ this._unmountingPromises.delete(appName);
8612
+ }
8613
+ })();
8614
+ this._unmountingPromises.set(appName, teardown);
7920
8615
  }
7921
8616
  }, 60));
7922
8617
  }
@@ -7965,6 +8660,10 @@ class WuCore {
7965
8660
  // Call plugin afterUnmount hooks
7966
8661
  await this.pluginSystem.callHook('afterUnmount', { appName });
7967
8662
 
8663
+ // Revoke this app's capability contracts with the framework-verified
8664
+ // app name (NOT from the spoofable app:unmounted bus event).
8665
+ try { this.contracts.revokeByApp(appName); } catch { /* best effort */ }
8666
+
7968
8667
  // Emit unmount event
7969
8668
  this.eventBus.emit('app:unmounted', { appName }, { appName });
7970
8669
 
@@ -8192,6 +8891,138 @@ class WuCore {
8192
8891
  };
8193
8892
  }
8194
8893
 
8894
+ /**
8895
+ * Register a capability another app can consume (typed, versioned contract).
8896
+ * @param {string} name
8897
+ * @param {object|Function} impl
8898
+ * @param {Object} [opts] - { version, shape, app }
8899
+ * @returns {{ revoke: Function }}
8900
+ * @see WuContracts#provide
8901
+ */
8902
+ provide(name, impl, opts) {
8903
+ return this.contracts.provide(name, impl, opts);
8904
+ }
8905
+
8906
+ /**
8907
+ * Consume a capability provided by another app. Returns a live proxy that
8908
+ * fails loudly on access if no provider satisfies the range (or a Promise
8909
+ * with { wait: true }).
8910
+ * @param {string} name
8911
+ * @param {string} [range='*']
8912
+ * @param {Object} [opts] - { wait, timeout }
8913
+ * @returns {Proxy|Promise<Proxy>}
8914
+ * @see WuContracts#consume
8915
+ */
8916
+ consume(name, range, opts) {
8917
+ return this.contracts.consume(name, range, opts);
8918
+ }
8919
+
8920
+ /**
8921
+ * Full diagnostic snapshot of the whole page — one structured object that
8922
+ * aggregates state scattered across mounted/hidden/definitions/apps, the
8923
+ * event bus, the store, and per-app sandbox info. The data backbone for
8924
+ * `wu.showInspector()` and the `window.__WU_DEVTOOLS__` bridge, and useful
8925
+ * on its own for debugging multi-framework pages where per-framework
8926
+ * devtools each see only their own island.
8927
+ *
8928
+ * @param {Object} [opts]
8929
+ * @param {number} [opts.events=25] - Recent events to include
8930
+ * @returns {Object} snapshot
8931
+ */
8932
+ inspect(opts = {}) {
8933
+ const { events = 25 } = opts;
8934
+
8935
+ const describeMount = (name, rec, status) => {
8936
+ const lifecycle = rec.lifecycle || this.definitions.get(name);
8937
+ return {
8938
+ name,
8939
+ status,
8940
+ framework: rec.app?.framework || rec.app?.config?.framework || null,
8941
+ containerSelector: rec.containerSelector || null,
8942
+ mountedAt: rec.timestamp || null,
8943
+ liveProps: typeof lifecycle?.update === 'function',
8944
+ props: rec.props || null,
8945
+ sandbox: this.getSandboxInfo(name),
8946
+ };
8947
+ };
8948
+
8949
+ const apps = [];
8950
+ for (const [name, rec] of this.mounted) apps.push(describeMount(name, rec, 'mounted'));
8951
+ for (const [name, rec] of this.hidden) apps.push(describeMount(name, rec, 'hidden'));
8952
+
8953
+ let eventHistory = [];
8954
+ try {
8955
+ const hist = this.eventBus.history || [];
8956
+ eventHistory = hist.slice(-events).map((e) => ({
8957
+ // WuEventBus stores the event under `name`; keep type/event fallbacks
8958
+ // for any external producer that uses a different field.
8959
+ type: e.name || e.type || e.event,
8960
+ appName: e.appName,
8961
+ timestamp: e.timestamp,
8962
+ }));
8963
+ } catch { /* bus may not expose history */ }
8964
+
8965
+ let storeSnapshot = null;
8966
+ try { storeSnapshot = this.store.get(); } catch { /* guarded */ }
8967
+
8968
+ return {
8969
+ version: this.version || null,
8970
+ timestamp: Date.now(),
8971
+ summary: {
8972
+ registered: this.apps.size,
8973
+ defined: this.definitions.size,
8974
+ mounted: this.mounted.size,
8975
+ hidden: this.hidden.size,
8976
+ },
8977
+ apps,
8978
+ defined: Array.from(this.definitions.keys()),
8979
+ registered: Array.from(this.apps.keys()),
8980
+ capabilities: (() => { try { return this.contracts.list(); } catch { return []; } })(),
8981
+ events: {
8982
+ recent: eventHistory,
8983
+ stats: (() => { try { return this.eventBus.getStats(); } catch { return null; } })(),
8984
+ },
8985
+ store: {
8986
+ snapshot: storeSnapshot,
8987
+ metrics: (() => { try { return this.store.getMetrics(); } catch { return null; } })(),
8988
+ },
8989
+ };
8990
+ }
8991
+
8992
+ /**
8993
+ * Open the visual inspector overlay (lazy-loaded, Shadow-DOM isolated so it
8994
+ * never collides with app styles). Returns a handle with .close().
8995
+ * @returns {Promise<{close: Function}|null>}
8996
+ */
8997
+ async showInspector() {
8998
+ if (typeof window === 'undefined') return null;
8999
+ const { mountInspector } = await import('./core/wu-devtools.js');
9000
+ return mountInspector(this);
9001
+ }
9002
+
9003
+ /**
9004
+ * Lazy-load and instantiate the time-travel recorder. Kept out of the main
9005
+ * bundle (like devtools/loader) — only paid for when something asks for
9006
+ * `wu.timeline`. Cached on `this._timeline`.
9007
+ * @private
9008
+ * @returns {Promise<import('./wu-timeline.js').WuTimeline>}
9009
+ */
9010
+ async _ensureTimeline() {
9011
+ if (this._timeline) return this._timeline;
9012
+ const { WuTimeline } = await import('./core/wu-timeline.js');
9013
+ this._timeline = new WuTimeline(this);
9014
+ return this._timeline;
9015
+ }
9016
+
9017
+ /** Close the inspector overlay if open. */
9018
+ async hideInspector() {
9019
+ if (typeof window === 'undefined') return;
9020
+ try {
9021
+ const { unmountInspector } = await import('./core/wu-devtools.js');
9022
+ unmountInspector();
9023
+ } catch { /* never opened */ }
9024
+ }
9025
+
8195
9026
  /**
8196
9027
  * Store methods: Convenience methods for state management
8197
9028
  */
@@ -8338,6 +9169,14 @@ class WuCore {
8338
9169
  logger.wuDebug('Destroying framework...');
8339
9170
 
8340
9171
  try {
9172
+ // Close the inspector overlay so its refresh timer doesn't outlive us
9173
+ await this.hideInspector();
9174
+
9175
+ // Stop the timeline recorder so its store/bus taps don't dangle.
9176
+ if (this._timeline) {
9177
+ try { this._timeline.stop(); } catch { /* best effort */ }
9178
+ }
9179
+
8341
9180
  // Execute beforeDestroy hooks
8342
9181
  await this.hooks.execute('beforeDestroy', {});
8343
9182
 
@@ -8350,6 +9189,7 @@ class WuCore {
8350
9189
  }
8351
9190
  this._pendingUnmounts.clear();
8352
9191
  this._mountingPromises.clear();
9192
+ this._unmountingPromises.clear();
8353
9193
 
8354
9194
  // Cancel and reject any pending define() waiters
8355
9195
  for (const [, waiter] of this._defineWaiters) {
@@ -8382,6 +9222,7 @@ class WuCore {
8382
9222
  this.errorBoundary.cleanup();
8383
9223
  this.hooks.cleanup();
8384
9224
  this.prefetcher.cleanup();
9225
+ this.contracts.cleanup();
8385
9226
 
8386
9227
  // Limpiar registros
8387
9228
  this.apps.clear();
@@ -8599,75 +9440,160 @@ if (typeof window !== 'undefined') {
8599
9440
  console.warn('[Wu Framework] window.wu already exists and is not a Wu Framework instance. Overwriting. Use Symbol.for("wu-framework") for collision-safe access.');
8600
9441
  }
8601
9442
  window.wu = wu;
9443
+ }
8602
9444
 
8603
- if (!wu.version) {
8604
- // "2.1.1" is replaced at build time with package.json version.
8605
- // Fallback handles raw-source imports (no bundler).
8606
- wu.version = "2.1.1" ;
8607
- wu.info = {
8608
- name: 'Wu Framework',
8609
- description: 'Universal Microfrontends',
8610
- features: ['Framework Agnostic', 'Zero Config', 'Shadow DOM Isolation', 'Runtime Loading']
8611
- };
8612
- }
9445
+ // Augment the instance everywhere (browser AND Node/SSR) — the d.ts declares
9446
+ // these members unconditionally on WuCore, so they must exist in any runtime.
9447
+ if (!wu.version) {
9448
+ // "2.5.0" is replaced at build time with package.json version.
9449
+ // Fallback handles raw-source imports (no bundler).
9450
+ wu.version = "2.5.0" ;
9451
+ wu.info = {
9452
+ name: 'Wu Framework',
9453
+ description: 'Universal Microfrontends',
9454
+ features: ['Framework Agnostic', 'Zero Config', 'Shadow DOM Isolation', 'Runtime Loading']
9455
+ };
9456
+ }
8613
9457
 
8614
- // Event Bus shortcuts on window.wu
8615
- if (!wu.emit) {
8616
- wu.emit = (event, data, opts) => wu.eventBus.emit(event, data, opts);
8617
- wu.on = (event, cb) => wu.eventBus.on(event, cb);
8618
- wu.once = (event, cb) => wu.eventBus.once(event, cb);
8619
- wu.off = (event, cb) => wu.eventBus.off(event, cb);
8620
- }
9458
+ // Event Bus shortcuts on wu
9459
+ if (!wu.emit) {
9460
+ wu.emit = (event, data, opts) => wu.eventBus.emit(event, data, opts);
9461
+ wu.on = (event, cb) => wu.eventBus.on(event, cb);
9462
+ wu.once = (event, cb) => wu.eventBus.once(event, cb);
9463
+ wu.off = (event, cb) => wu.eventBus.off(event, cb);
9464
+ }
8621
9465
 
8622
- // Prefetch shortcuts on window.wu
8623
- if (!wu.prefetch) {
8624
- wu.prefetch = (appNames, opts) => wu.prefetcher.prefetch(appNames, opts);
8625
- wu.prefetchAll = (opts) => wu.prefetcher.prefetchAll(opts);
8626
- }
9466
+ // Prefetch shortcuts on wu
9467
+ if (!wu.prefetch) {
9468
+ wu.prefetch = (appNames, opts) => wu.prefetcher.prefetch(appNames, opts);
9469
+ wu.prefetchAll = (opts) => wu.prefetcher.prefetchAll(opts);
9470
+ }
8627
9471
 
8628
- // Override shortcuts on window.wu
8629
- if (!wu.override) {
8630
- wu.override = (name, url, opts) => wu.overrides.set(name, url, opts);
8631
- wu.removeOverride = (name) => wu.overrides.remove(name);
8632
- wu.getOverrides = () => wu.overrides.getAll();
8633
- wu.clearOverrides = () => wu.overrides.clearAll();
8634
- }
9472
+ // Override shortcuts on wu
9473
+ if (!wu.override) {
9474
+ wu.override = (name, url, opts) => wu.overrides.set(name, url, opts);
9475
+ wu.removeOverride = (name) => wu.overrides.remove(name);
9476
+ wu.getOverrides = () => wu.overrides.getAll();
9477
+ wu.clearOverrides = () => wu.overrides.clearAll();
9478
+ }
8635
9479
 
8636
- // Log control: window.wu.silence() / window.wu.verbose()
8637
- if (!wu.silence) {
8638
- wu.silence = async () => { const { silenceAllLogs } = await Promise.resolve().then(function () { return wuLogger; }); silenceAllLogs(); };
8639
- wu.verbose = async () => { const { enableAllLogs } = await Promise.resolve().then(function () { return wuLogger; }); enableAllLogs(); };
8640
- }
9480
+ // Capability-contract shortcuts on wu (v2.5+)
9481
+ if (!wu.provide) {
9482
+ wu.provide = (name, impl, opts) => wu.contracts.provide(name, impl, opts);
9483
+ wu.consume = (name, range, opts) => wu.contracts.consume(name, range, opts);
9484
+ }
8641
9485
 
8642
- // AI integration lazy-loaded chunk + transparent proxy.
8643
- // The wu-ai subsystem (~80–100 KB minified) lives in a separate chunk
8644
- // and is only fetched on first access of `wu.ai.*`. Chainable config
8645
- // methods (provider/action/trigger/...) queue calls sync and return the
8646
- // proxy. Async methods (send/stream/agent/...) await the import + delegate.
8647
- // Use `await wu.aiReady()` to force load + get the real instance.
8648
- if (!wu.ai) {
8649
- setupLazyAi(wu);
8650
- }
9486
+ // Log control: wu.silence() / wu.verbose()
9487
+ if (!wu.silence) {
9488
+ wu.silence = async () => { const { silenceAllLogs } = await import('./core/wu-logger.js'); silenceAllLogs(); };
9489
+ wu.verbose = async () => { const { enableAllLogs } = await import('./core/wu-logger.js'); enableAllLogs(); };
9490
+ }
8651
9491
 
8652
- // MCP bridgeconnects to wu-mcp-server for AI agent control
8653
- if (!wu.mcp) {
8654
- let _mcpBridge = null;
8655
- wu.mcp = {
8656
- async connect(url = 'ws://localhost:19100', options = {}) {
8657
- if (!_mcpBridge) {
8658
- const { createMcpBridge } = await import('./core/wu-mcp-bridge.js');
8659
- _mcpBridge = createMcpBridge(wu);
8660
- }
8661
- _mcpBridge.connect(url, options);
8662
- },
8663
- disconnect() {
8664
- _mcpBridge?.disconnect();
8665
- },
8666
- isConnected() {
8667
- return _mcpBridge?.isConnected() || false;
8668
- },
8669
- };
8670
- }
9492
+ // AI integrationlazy-loaded chunk + transparent proxy.
9493
+ // The wu-ai subsystem (~80–100 KB minified) lives in a separate chunk
9494
+ // and is only fetched on first access of `wu.ai.*`. Chainable config
9495
+ // methods (provider/action/trigger/...) queue calls sync and return the
9496
+ // proxy. Async methods (send/stream/agent/...) await the import + delegate.
9497
+ // Use `await wu.aiReady()` to force load + get the real instance.
9498
+ if (!wu.ai) {
9499
+ setupLazyAi(wu);
9500
+ }
9501
+
9502
+ // DevTools bridge — a tiny always-present global so external inspectors (and
9503
+ // the built-in overlay) can detect Wu and pull a full-page snapshot. The
9504
+ // visual overlay itself is a lazy chunk, loaded only on show().
9505
+ if (typeof window !== 'undefined' && !window.__WU_DEVTOOLS__) {
9506
+ window.__WU_DEVTOOLS__ = {
9507
+ version: wu.version,
9508
+ isWu: true,
9509
+ inspect: (opts) => wu.inspect(opts),
9510
+ show: () => wu.showInspector(),
9511
+ hide: () => wu.hideInspector(),
9512
+ subscribe: (cb) => wu.eventBus.on('*', cb),
9513
+ };
9514
+ }
9515
+
9516
+ // MCP bridge — connects to wu-mcp-server for AI agent control
9517
+ if (!wu.mcp) {
9518
+ let _mcpBridge = null;
9519
+ wu.mcp = {
9520
+ async connect(url = 'ws://localhost:19100', options = {}) {
9521
+ if (!_mcpBridge) {
9522
+ const { createMcpBridge } = await import('./core/wu-mcp-bridge.js');
9523
+ _mcpBridge = createMcpBridge(wu);
9524
+ }
9525
+ _mcpBridge.connect(url, options);
9526
+ },
9527
+ disconnect() {
9528
+ _mcpBridge?.disconnect();
9529
+ },
9530
+ isConnected() {
9531
+ return _mcpBridge?.isConnected() || false;
9532
+ },
9533
+ };
9534
+ }
9535
+
9536
+ // Timeline — cross-framework time travel. Lazy chunk (loaded on first
9537
+ // `wu.timeline.*` call). The facade is synchronous and chainable for the
9538
+ // fire-and-forget controls (record/stop/clear) and returns Promises for the
9539
+ // seek family, mirroring the real WuTimeline API. `status()` answers without
9540
+ // forcing a load so devtools can poll it cheaply.
9541
+ //
9542
+ // Honest note: the recorder is a lazy chunk, so record() ARMS asynchronously —
9543
+ // store writes made before the chunk resolves are not journaled. record()/
9544
+ // stop() intent is tracked so a stop() issued before load cancels a pending
9545
+ // record() (no "recording after stop" surprise). For programmatic recording
9546
+ // that must capture the very next write, `await wu.timelineReady()` first
9547
+ // (mirrors wu.aiReady) — after that, record() arms synchronously. In the UI
9548
+ // path the chunk is already loaded (opening the inspector loads it), so the
9549
+ // Rec button arms immediately.
9550
+ if (!wu.timeline) {
9551
+ let _tl = null;
9552
+ let _loading = null;
9553
+ let _wantRecording = false;
9554
+ let _recordOpts;
9555
+ const ensure = () => {
9556
+ if (_tl) return Promise.resolve(_tl);
9557
+ if (!_loading) _loading = wu._ensureTimeline().then((t) => {
9558
+ _tl = t;
9559
+ // Apply the latest intent expressed before the chunk arrived.
9560
+ if (_wantRecording && !t.status().recording) t.record(_recordOpts);
9561
+ return t;
9562
+ });
9563
+ return _loading;
9564
+ };
9565
+ const NOT_LOADED = Object.freeze({
9566
+ loaded: false, recording: false, live: true, position: 0, length: 0,
9567
+ site: null, lamport: 0, snapshots: 0,
9568
+ });
9569
+ const facade = {
9570
+ record(opts) {
9571
+ _wantRecording = true; _recordOpts = opts;
9572
+ if (_tl) { if (!_tl.status().recording) _tl.record(opts); } else ensure();
9573
+ return facade;
9574
+ },
9575
+ stop() {
9576
+ _wantRecording = false; // cancels a record() still pending load
9577
+ if (_tl) _tl.stop();
9578
+ return facade;
9579
+ },
9580
+ clear() { ensure().then((t) => t.clear()); return facade; },
9581
+ seek(pos, o) { return ensure().then((t) => t.seek(pos, o)); },
9582
+ live() { return ensure().then((t) => t.live()); },
9583
+ stepBack() { return ensure().then((t) => t.stepBack()); },
9584
+ stepForward() { return ensure().then((t) => t.stepForward()); },
9585
+ export() { return ensure().then((t) => t.export()); },
9586
+ import(data) { return ensure().then((t) => t.import(data)); },
9587
+ ingest(entries) { return ensure().then((t) => t.ingest(entries)); },
9588
+ entries() { return _tl ? _tl.entries() : []; },
9589
+ status() { return _tl ? _tl.status() : { ...NOT_LOADED }; },
9590
+ get loaded() { return !!_tl; },
9591
+ get instance() { return _tl; },
9592
+ };
9593
+ wu.timeline = facade;
9594
+ // Warm-up: await it to force the chunk load + apply pending record intent,
9595
+ // then arm recording synchronously before writes. Resolves the WuTimeline.
9596
+ wu.timelineReady = ensure;
8671
9597
  }
8672
9598
  var wu_default = wu;
8673
9599
 
@@ -8694,6 +9620,8 @@ const off = (event, cb) => wu.eventBus.off(event, cb);
8694
9620
  const getState = (path) => wu.store.get(path);
8695
9621
  const setState = (path, value) => wu.store.set(path, value);
8696
9622
  const onStateChange = (pattern, cb) => wu.store.on(pattern, cb);
9623
+ // Real-time collaborative sync (v2.4+) — lazy CRDT layer over the store.
9624
+ const syncStore = (opts) => wu.store.sync(opts);
8697
9625
 
8698
9626
  // Performance
8699
9627
  const startMeasure = (name, app) => wu.performance.startMeasure(name, app);
@@ -8714,6 +9642,10 @@ const clearOverrides = () => wu.clearOverrides();
8714
9642
  const usePlugin = (plugin, opts) => wu.pluginSystem.use(plugin, opts);
8715
9643
  const useHook = (phase, middleware, opts) => wu.hooks.use(phase, middleware, opts);
8716
9644
 
9645
+ // Capability contracts (v2.5+)
9646
+ const provide = (name, impl, opts) => wu.contracts.provide(name, impl, opts);
9647
+ const consume = (name, range, opts) => wu.contracts.consume(name, range, opts);
9648
+
8717
9649
  // --- AI subsystem ---
8718
9650
  // AI classes were previously re-exported from this entry point. Doing so anchored
8719
9651
  // the ~80 KB AI chunk to the main bundle. As of v2.0, AI is loaded lazily via the
@@ -8726,5 +9658,5 @@ const useHook = (phase, middleware, opts) => wu.hooks.use(phase, middleware, opt
8726
9658
  //
8727
9659
  // For runtime use, prefer `wu.ai.*` — it auto-loads the chunk on first method call.
8728
9660
 
8729
- export { WuApp, WuCache, WuCore, WuErrorBoundary, WuEventBus, WuLifecycleHooks, WuLoadingStrategy, WuManifest, WuOverrides, WuPerformance, WuPluginSystem, WuPrefetch, WuProxySandbox, WuSandbox, WuStore, WuStyleBridge, app, clearOverrides, createConditionalHook, createGuardHook, createPlugin, createSimpleHook, createTimedHook, createTransformHook, wu_default as default, define, destroy, emit, enableAllLogs, endMeasure, generatePerformanceReport, getOverrides, getState, hide, init, isHidden, mount, normalizeStyleMode, off, on, onStateChange, once, override, prefetch, prefetchAll, removeOverride, setState, show, silenceAllLogs, startMeasure, store, unmount, useHook, usePlugin, wu };
9661
+ export { WuApp, WuCache, WuContracts, WuCore, WuErrorBoundary, WuEventBus, WuLifecycleHooks, WuLoadingStrategy, WuManifest, WuOverrides, WuPerformance, WuPluginSystem, WuPrefetch, WuProxySandbox, WuSandbox, WuStore, WuStyleBridge, app, clearOverrides, compareVersions, consume, createConditionalHook, createGuardHook, createPlugin, createSimpleHook, createTimedHook, createTransformHook, wu_default as default, define, destroy, emit, endMeasure, generatePerformanceReport, getOverrides, getState, hide, init, isHidden, mount, normalizeStyleMode, off, on, onStateChange, once, override, parseVersion, prefetch, prefetchAll, provide, removeOverride, satisfies, setState, show, startMeasure, store, syncStore, unmount, useHook, usePlugin, wu };
8730
9662
  //# sourceMappingURL=wu-framework.dev.js.map