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.
- package/README.md +6 -1
- package/dist/adapters/alpine/index.d.ts +1 -1
- package/dist/adapters/angular/index.d.ts +1 -1
- package/dist/adapters/htmx/index.d.ts +1 -1
- package/dist/adapters/lit/index.d.ts +1 -1
- package/dist/adapters/lit/index.js +2 -2
- package/dist/adapters/lit/index.js.map +1 -1
- package/dist/adapters/preact/index.d.ts +1 -1
- package/dist/adapters/preact/index.js +1 -1
- package/dist/adapters/preact/index.js.map +1 -1
- package/dist/adapters/qwik/index.d.ts +3 -10
- package/dist/adapters/qwik/index.js +1 -1
- package/dist/adapters/qwik/index.js.map +1 -1
- package/dist/adapters/react/index.js +1 -1
- package/dist/adapters/react/index.js.map +1 -1
- package/dist/adapters/shared.d.ts +44 -0
- package/dist/adapters/shared.js +1 -1
- package/dist/adapters/shared.js.map +1 -1
- package/dist/adapters/solid/index.d.ts +1 -1
- package/dist/adapters/solid/index.js +1 -1
- package/dist/adapters/solid/index.js.map +1 -1
- package/dist/adapters/stencil/index.d.ts +1 -1
- package/dist/adapters/stimulus/index.d.ts +1 -1
- package/dist/adapters/svelte/index.d.ts +1 -1
- package/dist/adapters/svelte/index.js +1 -1
- package/dist/adapters/svelte/index.js.map +1 -1
- package/dist/adapters/vanilla/index.d.ts +1 -1
- package/dist/adapters/vanilla/index.js +1 -1
- package/dist/adapters/vanilla/index.js.map +1 -1
- package/dist/adapters/vue/index.js +1 -1
- package/dist/adapters/vue/index.js.map +1 -1
- package/dist/ai/wu-ai.js +1 -1
- package/dist/ai/wu-ai.js.map +1 -1
- package/dist/core/wu-devtools.js +2 -0
- package/dist/core/wu-devtools.js.map +1 -0
- package/dist/core/wu-html-parser.js +1 -1
- package/dist/core/wu-html-parser.js.map +1 -1
- package/dist/core/wu-iframe-sandbox.js +1 -1
- package/dist/core/wu-iframe-sandbox.js.map +1 -1
- package/dist/core/wu-loader.js +1 -1
- package/dist/core/wu-loader.js.map +1 -1
- package/dist/core/wu-logger.js +2 -0
- package/dist/core/wu-logger.js.map +1 -0
- package/dist/core/wu-mcp-bridge.js +1 -1
- package/dist/core/wu-mcp-bridge.js.map +1 -1
- package/dist/core/wu-script-executor.js +1 -1
- package/dist/core/wu-script-executor.js.map +1 -1
- package/dist/core/wu-store-sync.js +2 -0
- package/dist/core/wu-store-sync.js.map +1 -0
- package/dist/core/wu-timeline.js +2 -0
- package/dist/core/wu-timeline.js.map +1 -0
- package/dist/index.d.cts +739 -0
- package/dist/index.d.ts +295 -1
- package/dist/wu-ai-browser-primitives-CaUCk1Xl.js +2 -0
- package/dist/wu-ai-browser-primitives-CaUCk1Xl.js.map +1 -0
- package/dist/wu-framework.cjs +3 -0
- package/dist/wu-framework.cjs.map +1 -0
- package/dist/wu-framework.dev.js +1207 -275
- package/dist/wu-framework.dev.js.map +1 -1
- package/dist/wu-framework.esm.js +2 -2
- package/dist/wu-framework.esm.js.map +1 -1
- package/dist/wu-framework.umd.js +2 -2
- package/dist/wu-framework.umd.js.map +1 -1
- package/integrations/astro/WuApp.astro +16 -11
- package/integrations/astro/WuShell.astro +11 -3
- package/package.json +14 -6
- package/dist/wu-ai-browser-primitives-BDKXJlwc.js +0 -2
- package/dist/wu-ai-browser-primitives-BDKXJlwc.js.map +0 -1
- package/dist/wu-framework.cjs.js +0 -3
- package/dist/wu-framework.cjs.js.map +0 -1
- package/dist/wu-logger-fJfUHBGA.js +0 -2
- package/dist/wu-logger-fJfUHBGA.js.map +0 -1
package/dist/wu-framework.dev.js
CHANGED
|
@@ -1,150 +1,8 @@
|
|
|
1
|
-
/*! wu-framework v2.
|
|
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 =
|
|
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
|
-
//
|
|
3265
|
-
this.
|
|
3266
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
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
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
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
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
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
|
-
|
|
7056
|
-
|
|
7057
|
-
|
|
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
|
-
|
|
7061
|
-
|
|
7062
|
-
|
|
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.
|
|
7177
|
-
sb.
|
|
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
|
-
//
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
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
|
-
|
|
8604
|
-
|
|
8605
|
-
|
|
8606
|
-
|
|
8607
|
-
|
|
8608
|
-
|
|
8609
|
-
|
|
8610
|
-
|
|
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
|
-
|
|
8615
|
-
|
|
8616
|
-
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
|
|
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
|
-
|
|
8623
|
-
|
|
8624
|
-
|
|
8625
|
-
|
|
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
|
-
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
|
|
8632
|
-
|
|
8633
|
-
|
|
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
|
-
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
|
|
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
|
-
|
|
8643
|
-
|
|
8644
|
-
|
|
8645
|
-
|
|
8646
|
-
|
|
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
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
8662
|
-
|
|
8663
|
-
|
|
8664
|
-
|
|
8665
|
-
|
|
8666
|
-
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
|
|
9492
|
+
// AI integration — lazy-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,
|
|
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
|