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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wu-framework",
3
- "version": "1.1.13",
3
+ "version": "1.1.14",
4
4
  "description": "Universal Microfrontends Framework - 8 frameworks, zero config, Shadow DOM isolation",
5
5
  "main": "dist/wu-framework.cjs.js",
6
6
  "module": "src/index.js",
@@ -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 inmediatamente (no diferido)
209
- doUnmount();
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
- // Función de unmount diferida espera 60ms antes de desmontar.
237
- // Si mountApp se llama antes de que expire el timer, el unmount se cancela.
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
- if (unmountTimer) clearTimeout(unmountTimer);
242
- unmountTimer = setTimeout(() => {
243
- unmountTimer = null;
244
- doUnmount(container);
245
- }, 60);
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
@@ -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
- return await this.mountWithRecovery(appName, containerSelector, 0);
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
- try {
844
- logger.wuDebug(`Unmounting ${appName}`);
878
+ logger.wuDebug(`Unmounting ${appName}`);
845
879
 
846
- const mounted = this.mounted.get(appName);
847
- if (!mounted) {
848
- // Check if it's hidden (keep-alive) — force destroy if requested
849
- if (options.force && this.hidden.has(appName)) {
850
- return await this._destroyHidden(appName);
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
- // Resolve keepAlive: per-call > per-app config > default false
857
- const keepAlive = options.force
858
- ? false
859
- : (options.keepAlive ?? mounted.app?.keepAlive ?? false);
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
- if (keepAlive) {
862
- return await this.hide(appName);
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);