wu-framework 1.1.12 → 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.
@@ -7363,6 +7363,8 @@ class WuCore {
7363
7363
  this.manifests = new Map(); // Manifiestos cargados
7364
7364
  this.mounted = new Map(); // Apps montadas
7365
7365
  this.hidden = new Map(); // Keep-alive hidden apps
7366
+ this._pendingUnmounts = new Map(); // Deferred unmount timers (StrictMode compat)
7367
+ this._mountingPromises = new Map(); // In-flight mount dedup
7366
7368
 
7367
7369
  // Componentes core
7368
7370
  this.loader = new WuLoader();
@@ -7511,6 +7513,31 @@ class WuCore {
7511
7513
  * @param {string} containerSelector - Selector del contenedor
7512
7514
  */
7513
7515
  async mount(appName, containerSelector) {
7516
+ // ── StrictMode guard: cancel pending deferred unmount ──
7517
+ // React StrictMode cycle: effect(mount) → cleanup(unmount) → effect(mount)
7518
+ // The cleanup fires between two mounts. By deferring the actual unmount,
7519
+ // the second mount cancels it and the app stays alive — zero flicker.
7520
+ if (this._pendingUnmounts.has(appName)) {
7521
+ clearTimeout(this._pendingUnmounts.get(appName));
7522
+ this._pendingUnmounts.delete(appName);
7523
+ logger.wuDebug(`${appName} deferred unmount cancelled by remount`);
7524
+ }
7525
+
7526
+ // Already mounted in same container → no-op
7527
+ if (this.mounted.has(appName)) {
7528
+ const existing = this.mounted.get(appName);
7529
+ if (existing.containerSelector === containerSelector) {
7530
+ logger.wuDebug(`${appName} already mounted in ${containerSelector}`);
7531
+ return;
7532
+ }
7533
+ }
7534
+
7535
+ // Deduplicate concurrent mounts (StrictMode fires effect twice)
7536
+ if (this._mountingPromises.has(appName)) {
7537
+ logger.wuDebug(`${appName} mount already in progress, deduplicating`);
7538
+ return await this._mountingPromises.get(appName);
7539
+ }
7540
+
7514
7541
  // Check if app is in keep-alive (hidden) state
7515
7542
  const hiddenEntry = this.hidden.get(appName);
7516
7543
  if (hiddenEntry) {
@@ -7522,7 +7549,15 @@ class WuCore {
7522
7549
  await this._destroyHidden(appName);
7523
7550
  }
7524
7551
 
7525
- return await this.mountWithRecovery(appName, containerSelector, 0);
7552
+ // Track mount promise for deduplication
7553
+ const mountPromise = this.mountWithRecovery(appName, containerSelector, 0);
7554
+ this._mountingPromises.set(appName, mountPromise);
7555
+
7556
+ try {
7557
+ return await mountPromise;
7558
+ } finally {
7559
+ this._mountingPromises.delete(appName);
7560
+ }
7526
7561
  }
7527
7562
 
7528
7563
  /**
@@ -8173,28 +8208,60 @@ class WuCore {
8173
8208
  * @param {boolean} [options.force] - Force destroy even if keepAlive
8174
8209
  */
8175
8210
  async unmount(appName, options = {}) {
8176
- try {
8177
- logger.wuDebug(`Unmounting ${appName}`);
8211
+ logger.wuDebug(`Unmounting ${appName}`);
8178
8212
 
8179
- const mounted = this.mounted.get(appName);
8180
- if (!mounted) {
8181
- // Check if it's hidden (keep-alive) — force destroy if requested
8182
- if (options.force && this.hidden.has(appName)) {
8183
- return await this._destroyHidden(appName);
8184
- }
8185
- logger.wuWarn(`App ${appName} not mounted`);
8186
- return;
8213
+ const mounted = this.mounted.get(appName);
8214
+ if (!mounted) {
8215
+ // Check if it's hidden (keep-alive) — force destroy if requested
8216
+ if (options.force && this.hidden.has(appName)) {
8217
+ return await this._destroyHidden(appName);
8187
8218
  }
8219
+ logger.wuWarn(`App ${appName} not mounted`);
8220
+ return;
8221
+ }
8188
8222
 
8189
- // Resolve keepAlive: per-call > per-app config > default false
8190
- const keepAlive = options.force
8191
- ? false
8192
- : (options.keepAlive ?? mounted.app?.keepAlive ?? false);
8223
+ // Resolve keepAlive: per-call > per-app config > default false
8224
+ const keepAlive = options.force
8225
+ ? false
8226
+ : (options.keepAlive ?? mounted.app?.keepAlive ?? false);
8193
8227
 
8194
- if (keepAlive) {
8195
- return await this.hide(appName);
8228
+ if (keepAlive) {
8229
+ return await this.hide(appName);
8230
+ }
8231
+
8232
+ // Force → immediate unmount (no deferral)
8233
+ if (options.force) {
8234
+ return await this._executeUnmount(appName, mounted);
8235
+ }
8236
+
8237
+ // ── Deferred unmount: 60ms window for React StrictMode ──
8238
+ // StrictMode cycle: effect(mount) → cleanup(unmount) → effect(mount)
8239
+ // The cleanup fires between two mounts. By deferring the actual unmount,
8240
+ // the second mount() call cancels the timer and the app stays alive.
8241
+ if (this._pendingUnmounts.has(appName)) {
8242
+ clearTimeout(this._pendingUnmounts.get(appName));
8243
+ }
8244
+
8245
+ this._pendingUnmounts.set(appName, setTimeout(async () => {
8246
+ this._pendingUnmounts.delete(appName);
8247
+ // Re-verify: only unmount if the same mount entry is still current
8248
+ if (this.mounted.has(appName) && this.mounted.get(appName) === mounted) {
8249
+ try {
8250
+ await this._executeUnmount(appName, mounted);
8251
+ } catch (error) {
8252
+ logger.wuError(`Deferred unmount failed for ${appName}:`, error);
8253
+ }
8196
8254
  }
8255
+ }, 60));
8256
+ }
8197
8257
 
8258
+ /**
8259
+ * Execute the actual unmount immediately (no deferral).
8260
+ * Called by the deferred timer, force unmount, or destroy.
8261
+ * @private
8262
+ */
8263
+ async _executeUnmount(appName, mounted) {
8264
+ try {
8198
8265
  // Execute beforeUnmount hooks
8199
8266
  const beforeUnmountResult = await this.hooks.execute('beforeUnmount', { appName, mounted });
8200
8267
  if (beforeUnmountResult.cancelled) {
@@ -8610,6 +8677,13 @@ class WuCore {
8610
8677
  // Call plugin onDestroy hooks
8611
8678
  await this.pluginSystem.callHook('onDestroy', {});
8612
8679
 
8680
+ // Cancel all pending deferred unmounts
8681
+ for (const timer of this._pendingUnmounts.values()) {
8682
+ clearTimeout(timer);
8683
+ }
8684
+ this._pendingUnmounts.clear();
8685
+ this._mountingPromises.clear();
8686
+
8613
8687
  // Force-destroy all hidden (keep-alive) apps first
8614
8688
  for (const appName of [...this.hidden.keys()]) {
8615
8689
  await this._destroyHidden(appName);