wu-framework 1.1.13 → 1.1.14
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/dist/wu-framework.cjs.js +1 -1
- package/dist/wu-framework.cjs.js.map +1 -1
- package/dist/wu-framework.dev.js +91 -17
- package/dist/wu-framework.dev.js.map +1 -1
- package/dist/wu-framework.esm.js +1 -1
- package/dist/wu-framework.esm.js.map +1 -1
- package/dist/wu-framework.umd.js +1 -1
- package/dist/wu-framework.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/react/index.js +16 -39
- package/src/core/wu-core.js +91 -17
package/package.json
CHANGED
|
@@ -167,25 +167,6 @@ async function register(appName, Component, options = {}) {
|
|
|
167
167
|
|
|
168
168
|
const { React, createRoot } = adapterState;
|
|
169
169
|
|
|
170
|
-
// Timer para diferir el unmount — permite que StrictMode remount lo cancele
|
|
171
|
-
let unmountTimer = null;
|
|
172
|
-
|
|
173
|
-
// Unmount real (sin defer)
|
|
174
|
-
const doUnmount = (container) => {
|
|
175
|
-
const instance = adapterState.roots.get(appName);
|
|
176
|
-
if (instance) {
|
|
177
|
-
try {
|
|
178
|
-
if (onUnmount) onUnmount(instance.container);
|
|
179
|
-
instance.root.unmount();
|
|
180
|
-
adapterState.roots.delete(appName);
|
|
181
|
-
} catch (error) {
|
|
182
|
-
console.error(`[WuReact] Unmount error for ${appName}:`, error);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
const target = container || instance?.container;
|
|
186
|
-
if (target) target.innerHTML = '';
|
|
187
|
-
};
|
|
188
|
-
|
|
189
170
|
// Función de mount interna
|
|
190
171
|
const mountApp = (container) => {
|
|
191
172
|
if (!container) {
|
|
@@ -193,27 +174,18 @@ async function register(appName, Component, options = {}) {
|
|
|
193
174
|
return;
|
|
194
175
|
}
|
|
195
176
|
|
|
196
|
-
// Cancelar cualquier unmount diferido (patrón StrictMode: mount → unmount → remount)
|
|
197
|
-
if (unmountTimer) {
|
|
198
|
-
clearTimeout(unmountTimer);
|
|
199
|
-
unmountTimer = null;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
177
|
// Si ya está montado en el MISMO container, ignorar
|
|
203
178
|
const existing = adapterState.roots.get(appName);
|
|
204
179
|
if (existing) {
|
|
205
180
|
if (existing.container === container) {
|
|
206
181
|
return; // Ya montado aquí, nada que hacer
|
|
207
182
|
}
|
|
208
|
-
// Diferente container → desmontar
|
|
209
|
-
|
|
183
|
+
// Diferente container → desmontar primero
|
|
184
|
+
unmountApp();
|
|
210
185
|
}
|
|
211
186
|
|
|
212
187
|
try {
|
|
213
|
-
// Limpiar el DOM antes de crear un nuevo root.
|
|
214
|
-
// root.unmount() de React 18+ es async — innerHTML = '' fuerza DOM limpio.
|
|
215
188
|
container.innerHTML = '';
|
|
216
|
-
|
|
217
189
|
const root = createRoot(container);
|
|
218
190
|
|
|
219
191
|
let element = React.createElement(Component, props);
|
|
@@ -233,16 +205,21 @@ async function register(appName, Component, options = {}) {
|
|
|
233
205
|
}
|
|
234
206
|
};
|
|
235
207
|
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
// Esto resuelve el ciclo StrictMode: effect(mount) → cleanup(unmount) → effect(mount)
|
|
239
|
-
// donde el cleanup ocurre ENTRE los dos mounts async.
|
|
208
|
+
// Unmount inmediato — la protección contra StrictMode (deferred unmount)
|
|
209
|
+
// se maneja en wu-core.js, no aquí en el adapter.
|
|
240
210
|
const unmountApp = (container) => {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
211
|
+
const instance = adapterState.roots.get(appName);
|
|
212
|
+
if (instance) {
|
|
213
|
+
try {
|
|
214
|
+
if (onUnmount) onUnmount(instance.container);
|
|
215
|
+
instance.root.unmount();
|
|
216
|
+
adapterState.roots.delete(appName);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.error(`[WuReact] Unmount error for ${appName}:`, error);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const target = container || instance?.container;
|
|
222
|
+
if (target) target.innerHTML = '';
|
|
246
223
|
};
|
|
247
224
|
|
|
248
225
|
// Intentar registrar con Wu Framework
|
package/src/core/wu-core.js
CHANGED
|
@@ -30,6 +30,8 @@ export class WuCore {
|
|
|
30
30
|
this.manifests = new Map(); // Manifiestos cargados
|
|
31
31
|
this.mounted = new Map(); // Apps montadas
|
|
32
32
|
this.hidden = new Map(); // Keep-alive hidden apps
|
|
33
|
+
this._pendingUnmounts = new Map(); // Deferred unmount timers (StrictMode compat)
|
|
34
|
+
this._mountingPromises = new Map(); // In-flight mount dedup
|
|
33
35
|
|
|
34
36
|
// Componentes core
|
|
35
37
|
this.loader = new WuLoader();
|
|
@@ -178,6 +180,31 @@ export class WuCore {
|
|
|
178
180
|
* @param {string} containerSelector - Selector del contenedor
|
|
179
181
|
*/
|
|
180
182
|
async mount(appName, containerSelector) {
|
|
183
|
+
// ── StrictMode guard: cancel pending deferred unmount ──
|
|
184
|
+
// React StrictMode cycle: effect(mount) → cleanup(unmount) → effect(mount)
|
|
185
|
+
// The cleanup fires between two mounts. By deferring the actual unmount,
|
|
186
|
+
// the second mount cancels it and the app stays alive — zero flicker.
|
|
187
|
+
if (this._pendingUnmounts.has(appName)) {
|
|
188
|
+
clearTimeout(this._pendingUnmounts.get(appName));
|
|
189
|
+
this._pendingUnmounts.delete(appName);
|
|
190
|
+
logger.wuDebug(`${appName} deferred unmount cancelled by remount`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Already mounted in same container → no-op
|
|
194
|
+
if (this.mounted.has(appName)) {
|
|
195
|
+
const existing = this.mounted.get(appName);
|
|
196
|
+
if (existing.containerSelector === containerSelector) {
|
|
197
|
+
logger.wuDebug(`${appName} already mounted in ${containerSelector}`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Deduplicate concurrent mounts (StrictMode fires effect twice)
|
|
203
|
+
if (this._mountingPromises.has(appName)) {
|
|
204
|
+
logger.wuDebug(`${appName} mount already in progress, deduplicating`);
|
|
205
|
+
return await this._mountingPromises.get(appName);
|
|
206
|
+
}
|
|
207
|
+
|
|
181
208
|
// Check if app is in keep-alive (hidden) state
|
|
182
209
|
const hiddenEntry = this.hidden.get(appName);
|
|
183
210
|
if (hiddenEntry) {
|
|
@@ -189,7 +216,15 @@ export class WuCore {
|
|
|
189
216
|
await this._destroyHidden(appName);
|
|
190
217
|
}
|
|
191
218
|
|
|
192
|
-
|
|
219
|
+
// Track mount promise for deduplication
|
|
220
|
+
const mountPromise = this.mountWithRecovery(appName, containerSelector, 0);
|
|
221
|
+
this._mountingPromises.set(appName, mountPromise);
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
return await mountPromise;
|
|
225
|
+
} finally {
|
|
226
|
+
this._mountingPromises.delete(appName);
|
|
227
|
+
}
|
|
193
228
|
}
|
|
194
229
|
|
|
195
230
|
/**
|
|
@@ -840,28 +875,60 @@ export class WuCore {
|
|
|
840
875
|
* @param {boolean} [options.force] - Force destroy even if keepAlive
|
|
841
876
|
*/
|
|
842
877
|
async unmount(appName, options = {}) {
|
|
843
|
-
|
|
844
|
-
logger.wuDebug(`Unmounting ${appName}`);
|
|
878
|
+
logger.wuDebug(`Unmounting ${appName}`);
|
|
845
879
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
}
|
|
852
|
-
logger.wuWarn(`App ${appName} not mounted`);
|
|
853
|
-
return;
|
|
880
|
+
const mounted = this.mounted.get(appName);
|
|
881
|
+
if (!mounted) {
|
|
882
|
+
// Check if it's hidden (keep-alive) — force destroy if requested
|
|
883
|
+
if (options.force && this.hidden.has(appName)) {
|
|
884
|
+
return await this._destroyHidden(appName);
|
|
854
885
|
}
|
|
886
|
+
logger.wuWarn(`App ${appName} not mounted`);
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
855
889
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
890
|
+
// Resolve keepAlive: per-call > per-app config > default false
|
|
891
|
+
const keepAlive = options.force
|
|
892
|
+
? false
|
|
893
|
+
: (options.keepAlive ?? mounted.app?.keepAlive ?? false);
|
|
860
894
|
|
|
861
|
-
|
|
862
|
-
|
|
895
|
+
if (keepAlive) {
|
|
896
|
+
return await this.hide(appName);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Force → immediate unmount (no deferral)
|
|
900
|
+
if (options.force) {
|
|
901
|
+
return await this._executeUnmount(appName, mounted);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// ── Deferred unmount: 60ms window for React StrictMode ──
|
|
905
|
+
// StrictMode cycle: effect(mount) → cleanup(unmount) → effect(mount)
|
|
906
|
+
// The cleanup fires between two mounts. By deferring the actual unmount,
|
|
907
|
+
// the second mount() call cancels the timer and the app stays alive.
|
|
908
|
+
if (this._pendingUnmounts.has(appName)) {
|
|
909
|
+
clearTimeout(this._pendingUnmounts.get(appName));
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
this._pendingUnmounts.set(appName, setTimeout(async () => {
|
|
913
|
+
this._pendingUnmounts.delete(appName);
|
|
914
|
+
// Re-verify: only unmount if the same mount entry is still current
|
|
915
|
+
if (this.mounted.has(appName) && this.mounted.get(appName) === mounted) {
|
|
916
|
+
try {
|
|
917
|
+
await this._executeUnmount(appName, mounted);
|
|
918
|
+
} catch (error) {
|
|
919
|
+
logger.wuError(`Deferred unmount failed for ${appName}:`, error);
|
|
920
|
+
}
|
|
863
921
|
}
|
|
922
|
+
}, 60));
|
|
923
|
+
}
|
|
864
924
|
|
|
925
|
+
/**
|
|
926
|
+
* Execute the actual unmount immediately (no deferral).
|
|
927
|
+
* Called by the deferred timer, force unmount, or destroy.
|
|
928
|
+
* @private
|
|
929
|
+
*/
|
|
930
|
+
async _executeUnmount(appName, mounted) {
|
|
931
|
+
try {
|
|
865
932
|
// Execute beforeUnmount hooks
|
|
866
933
|
const beforeUnmountResult = await this.hooks.execute('beforeUnmount', { appName, mounted });
|
|
867
934
|
if (beforeUnmountResult.cancelled) {
|
|
@@ -1277,6 +1344,13 @@ export class WuCore {
|
|
|
1277
1344
|
// Call plugin onDestroy hooks
|
|
1278
1345
|
await this.pluginSystem.callHook('onDestroy', {});
|
|
1279
1346
|
|
|
1347
|
+
// Cancel all pending deferred unmounts
|
|
1348
|
+
for (const timer of this._pendingUnmounts.values()) {
|
|
1349
|
+
clearTimeout(timer);
|
|
1350
|
+
}
|
|
1351
|
+
this._pendingUnmounts.clear();
|
|
1352
|
+
this._mountingPromises.clear();
|
|
1353
|
+
|
|
1280
1354
|
// Force-destroy all hidden (keep-alive) apps first
|
|
1281
1355
|
for (const appName of [...this.hidden.keys()]) {
|
|
1282
1356
|
await this._destroyHidden(appName);
|