@softarc/native-federation-orchestrator 4.6.0 → 4.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +2 -2
  2. package/fesm2022/@softarc/native-federation-orchestrator.mjs +340 -111
  3. package/fesm2022/@softarc/native-federation-orchestrator.mjs.map +4 -4
  4. package/fesm2022/node.mjs +231 -72
  5. package/fesm2022/node.mjs.map +4 -4
  6. package/fesm2022/options.mjs +6 -0
  7. package/fesm2022/options.mjs.map +2 -2
  8. package/fesm2022/sdk.mjs.map +1 -1
  9. package/node-loader/loader.mjs +26 -6
  10. package/node-loader/loader.mjs.map +2 -2
  11. package/package.json +1 -1
  12. package/quickstart.mjs +15 -15
  13. package/types/lib/core/1.domain/externals/external.contract.d.ts +1 -0
  14. package/types/lib/core/1.domain/externals/version.contract.d.ts +2 -0
  15. package/types/lib/core/2.app/config/index.d.ts +2 -1
  16. package/types/lib/core/2.app/config/mode.contract.d.ts +0 -1
  17. package/types/lib/core/2.app/config/orchestrator-global.contract.d.ts +15 -0
  18. package/types/lib/core/2.app/config/storage.contract.d.ts +14 -2
  19. package/types/lib/core/2.app/driving-ports/for-shared-externals-storage.port.d.ts +1 -1
  20. package/types/lib/core/2.app/steps/pooling/pool-graph.d.ts +7 -12
  21. package/types/lib/core/2.app/steps/pooling/pool.util.d.ts +12 -11
  22. package/types/lib/core/3.adapters/browser/sse-handler.d.ts +0 -8
  23. package/types/lib/core/3.adapters/storage/chunk.repository.d.ts +1 -1
  24. package/types/lib/core/3.adapters/storage/remote-info.repository.d.ts +1 -1
  25. package/types/lib/core/3.adapters/storage/scoped-externals.repository.d.ts +1 -1
  26. package/types/lib/core/3.adapters/storage/shared-externals.repository.d.ts +1 -1
  27. package/types/lib/core/4.config/storage/publish-storage-info.d.ts +9 -0
  28. package/types/lib/testing/pooling/tag-by-npm-scope.d.ts +9 -0
  29. package/types/scripts/node-loader.d.ts +1 -0
@@ -1577,7 +1577,7 @@ function createStoreRemoteEntry(config, ports, logStep) {
1577
1577
  ...sharedInfo.pool?.trim() ? { pool: sharedInfo.pool } : {},
1578
1578
  entries: sharedInfo.entries
1579
1579
  };
1580
- const cached = ports.sharedExternalsRepo.tryGet(sharedInfo.packageName, sharedInfo.shareScope).orElse({ dirty: false, versions: [] });
1580
+ const cached = ports.sharedExternalsRepo.tryGet(sharedInfo.packageName, sharedInfo.shareScope).get() ?? { dirty: false, versions: [] };
1581
1581
  return {
1582
1582
  tag,
1583
1583
  remote,
@@ -1598,6 +1598,7 @@ function createStoreRemoteEntry(config, ports, logStep) {
1598
1598
  sharedInfo.packageName,
1599
1599
  {
1600
1600
  dirty: cached.dirty,
1601
+ ...cached.poolName !== void 0 && { poolName: cached.poolName },
1601
1602
  versions: cached.versions.sort((a, b) => ports.versionCheck.compare(b.tag, a.tag))
1602
1603
  },
1603
1604
  sharedInfo.shareScope
@@ -2199,7 +2200,6 @@ function assignAnchors(input) {
2199
2200
  }
2200
2201
 
2201
2202
  // src/lib/core/2.app/steps/pooling/pool-graph.ts
2202
- var SCOPED_PACKAGE = /^@([^/]+)\//;
2203
2203
  function createDSU() {
2204
2204
  const ids = /* @__PURE__ */ new Map();
2205
2205
  const parent = [];
@@ -2238,10 +2238,6 @@ function createDSU() {
2238
2238
  }
2239
2239
  var extNode = (name) => `ext\0${name}`;
2240
2240
  var tagNode = (remote, tag) => `tag\0${remote}\0${tag}`;
2241
- var scopeNode = (remote, scope) => `scope\0${remote}\0${scope}`;
2242
- function autoScope(name, useAutoExternalPooling) {
2243
- return useAutoExternalPooling ? SCOPED_PACKAGE.exec(name)?.[1] : void 0;
2244
- }
2245
2241
  function owningPackage(name) {
2246
2242
  const depth = name.startsWith("@") ? 2 : 1;
2247
2243
  let cut = -1;
@@ -2254,23 +2250,10 @@ function owningPackage(name) {
2254
2250
  function groupByMembership(candidates, log) {
2255
2251
  const dsu = createDSU();
2256
2252
  const tagged = /* @__PURE__ */ new Set();
2257
- const joined = /* @__PURE__ */ new Set();
2258
- const tagRules = /* @__PURE__ */ new Set();
2259
- for (const candidate of candidates) {
2260
- if (candidate.scope === void 0) continue;
2261
- for (const edge of candidate.tags) tagRules.add(`${edge.remote}\0${candidate.scope}`);
2262
- }
2263
2253
  for (const candidate of candidates) {
2264
2254
  for (const edge of candidate.tags) {
2265
2255
  dsu.union(extNode(candidate.name), tagNode(edge.remote, edge.tag));
2266
2256
  tagged.add(candidate.name);
2267
- joined.add(candidate.name);
2268
- }
2269
- if (candidate.scope === void 0) continue;
2270
- for (const remote of candidate.remotes) {
2271
- if (tagRules.has(`${remote}\0${candidate.scope}`)) continue;
2272
- dsu.union(extNode(candidate.name), scopeNode(remote, candidate.scope));
2273
- joined.add(candidate.name);
2274
2257
  }
2275
2258
  }
2276
2259
  const declared = new Set(candidates.map((c) => c.name));
@@ -2283,11 +2266,11 @@ function groupByMembership(candidates, log) {
2283
2266
  for (const candidate of candidates) {
2284
2267
  const root = dsu.component(extNode(candidate.name));
2285
2268
  const members = byComponent.get(root) ?? byComponent.set(root, []).get(root);
2286
- members.push({ name: candidate.name, value: candidate.value });
2269
+ members.push(candidate);
2287
2270
  }
2288
2271
  const pools = [];
2289
2272
  for (const members of byComponent.values()) {
2290
- if (!members.some((m) => joined.has(m.name))) continue;
2273
+ if (!members.some((m) => tagged.has(m.name))) continue;
2291
2274
  members.sort((a, b) => a.name.localeCompare(b.name));
2292
2275
  if (members.length < 2) {
2293
2276
  const only = members[0];
@@ -2302,20 +2285,39 @@ function groupByMembership(candidates, log) {
2302
2285
  pools.push(members);
2303
2286
  }
2304
2287
  pools.sort((a, b) => a[0].name.localeCompare(b[0].name));
2305
- return new Map(pools.map((members) => [members[0].name, members.map((m) => m.value)]));
2288
+ const named = /* @__PURE__ */ new Map();
2289
+ for (const members of pools) {
2290
+ const label = mostDeclaredTag(members) ?? members[0].name;
2291
+ let name = label;
2292
+ for (let n = 2; named.has(name); n++) name = `${label}~${n}`;
2293
+ named.set(
2294
+ name,
2295
+ members.map((m) => m.value)
2296
+ );
2297
+ }
2298
+ return named;
2299
+ }
2300
+ function mostDeclaredTag(members) {
2301
+ const counts = /* @__PURE__ */ new Map();
2302
+ for (const member of members)
2303
+ for (const edge of member.tags) counts.set(edge.tag, (counts.get(edge.tag) ?? 0) + 1);
2304
+ let best;
2305
+ for (const [tag, count] of counts) {
2306
+ const top = best === void 0 ? 0 : counts.get(best);
2307
+ if (count > top || count === top && tag.localeCompare(best) < 0) best = tag;
2308
+ }
2309
+ return best;
2306
2310
  }
2307
- function buildPools(sharedExternals, useAutoExternalPooling, log) {
2311
+ function buildPools(sharedExternals, log) {
2308
2312
  const candidates = Object.entries(sharedExternals).map(
2309
2313
  ([name, external]) => ({
2310
2314
  name,
2311
- scope: autoScope(name, useAutoExternalPooling),
2312
2315
  tags: external.versions.flatMap(
2313
2316
  (v) => v.remotes.flatMap((r) => {
2314
2317
  const tag = r.pool?.trim();
2315
2318
  return tag ? [{ remote: r.name, tag }] : [];
2316
2319
  })
2317
2320
  ),
2318
- remotes: external.versions.flatMap((v) => v.remotes.map((r) => r.name)),
2319
2321
  value: { name, external }
2320
2322
  })
2321
2323
  );
@@ -2332,14 +2334,29 @@ function remotesInPool(members) {
2332
2334
  ...new Set(members.flatMap((m) => m.external.versions.flatMap((v) => v.remotes.map((r) => r.name))))
2333
2335
  ];
2334
2336
  }
2335
- function poolableScopes(config, repo) {
2336
- const { useAutoExternalPooling } = config.feature;
2337
- return {
2338
- useAutoExternalPooling,
2339
- scopes: repo.getScopes().filter(
2340
- (scope) => repo.scopeType(scope) !== "strict" && (useAutoExternalPooling || repo.hasPoolTag(scope))
2341
- )
2342
- };
2337
+ function poolableScopes(repo) {
2338
+ return repo.getScopes().filter((scope) => repo.scopeType(scope) !== "strict" && repo.hasPoolState(scope));
2339
+ }
2340
+ function syncPoolNames(sharedExternals, pools, repo, scope, skip = /* @__PURE__ */ new Set()) {
2341
+ const named = /* @__PURE__ */ new Map();
2342
+ for (const [name, members] of pools) for (const member of members) named.set(member.name, name);
2343
+ for (const [name, external] of Object.entries(sharedExternals)) {
2344
+ const pool = named.get(name);
2345
+ if (pool !== void 0 && skip.has(pool)) continue;
2346
+ if (external.poolName === pool) continue;
2347
+ const { poolName: _stale, ...rest } = external;
2348
+ repo.addOrUpdate(
2349
+ name,
2350
+ pool !== void 0 ? { ...rest, poolName: pool } : {
2351
+ ...rest,
2352
+ versions: external.versions.map((v) => ({
2353
+ ...v,
2354
+ remotes: v.remotes.map(({ poolCause: _cause, ...meta }) => meta)
2355
+ }))
2356
+ },
2357
+ scope
2358
+ );
2359
+ }
2343
2360
  }
2344
2361
 
2345
2362
  // src/lib/core/2.app/steps/pooling/pool-shared-externals.ts
@@ -2373,7 +2390,11 @@ function servedPerRemote(consumed, serving, basis) {
2373
2390
  }
2374
2391
  return served;
2375
2392
  }
2376
- var anyAnchorStored = (members) => members.some((m) => m.external.versions.some((v) => v.remotes.some((r) => r.servedBy !== void 0)));
2393
+ var anyVerdictStored = (members) => members.some(
2394
+ (m) => m.external.versions.some(
2395
+ (v) => v.remotes.some((r) => r.servedBy !== void 0 || r.poolCause !== void 0)
2396
+ )
2397
+ );
2377
2398
  function findTornRemotes(members, islanded, basis, served, hosts) {
2378
2399
  const { suspect, selfServed } = movedRemotes(members, islanded, basis, served, hosts);
2379
2400
  if (suspect.size === 0) return [];
@@ -2444,20 +2465,18 @@ function movedRemotes(members, islanded, basis, served, hosts) {
2444
2465
  }
2445
2466
  function createPoolSharedExternals(config, ports) {
2446
2467
  return (touched) => {
2447
- const { useAutoExternalPooling, scopes } = poolableScopes(config, ports.sharedExternalsRepo);
2448
- for (const scope of scopes) {
2468
+ for (const scope of poolableScopes(ports.sharedExternalsRepo)) {
2449
2469
  const touchedInScope = touched?.get(scope);
2450
2470
  if (touched && !touchedInScope) continue;
2451
2471
  const sharedExternals = ports.sharedExternalsRepo.getFromScope(scope);
2452
2472
  try {
2453
- for (const [poolName, members] of buildPools(
2454
- sharedExternals,
2455
- useAutoExternalPooling,
2456
- config.log
2457
- )) {
2473
+ const pools = buildPools(sharedExternals, config.log);
2474
+ const rebuilt = /* @__PURE__ */ new Set();
2475
+ for (const [poolName, members] of pools) {
2458
2476
  if (touchedInScope && !members.some((m) => touchedInScope.has(m.name))) continue;
2459
- poolFamily(poolName, members, scope);
2477
+ if (poolFamily(poolName, members, scope)) rebuilt.add(poolName);
2460
2478
  }
2479
+ syncPoolNames(sharedExternals, pools, ports.sharedExternalsRepo, scope, rebuilt);
2461
2480
  } catch (error) {
2462
2481
  if (error instanceof NFError) return Promise.reject(error);
2463
2482
  config.log.error(3, `[${scope}] failed to pool shared externals.`, {
@@ -2472,9 +2491,19 @@ function createPoolSharedExternals(config, ports) {
2472
2491
  return Promise.resolve();
2473
2492
  };
2474
2493
  function poolFamily(poolName, members, scope) {
2475
- if (members.length < 2) return;
2494
+ if (members.length < 2) return false;
2476
2495
  const allRemotes = remotesInPool(members);
2477
- if (allRemotes.length < 2) return;
2496
+ if (allRemotes.length < 2) {
2497
+ if (!anyVerdictStored(members)) return false;
2498
+ const basis2 = basisFor(members, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Map());
2499
+ for (const member of members)
2500
+ ports.sharedExternalsRepo.addOrUpdate(
2501
+ member.name,
2502
+ rebuildMember(poolName, member, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), basis2.get(member.name)),
2503
+ scope
2504
+ );
2505
+ return true;
2506
+ }
2478
2507
  const islanded = islandedRemotes(members);
2479
2508
  const consumed = consumedMembers(members);
2480
2509
  const hosts = hostRemotes(members);
@@ -2494,7 +2523,7 @@ function createPoolSharedExternals(config, ports) {
2494
2523
  return { serving: serving2, basis: basis2, served: servedPerRemote(consumed, serving2, basis2) };
2495
2524
  };
2496
2525
  let { serving, basis, served } = assign();
2497
- if (islanded.size === 0 && serving.size === 0 && !anyAnchorStored(members)) return;
2526
+ if (islanded.size === 0 && serving.size === 0 && !anyVerdictStored(members)) return false;
2498
2527
  for (; ; ) {
2499
2528
  const torn = findTornRemotes(members, islanded, basis, served, hosts);
2500
2529
  if (torn.length === 0) break;
@@ -2515,10 +2544,11 @@ function createPoolSharedExternals(config, ports) {
2515
2544
  config.log.warn(3, `[${scope}][pool:${poolName}] ${islandWarning(remote, cause, scoped)}`);
2516
2545
  }
2517
2546
  for (const member of members) {
2518
- const rebuilt = rebuildMember(member, islanded, served, basis.get(member.name));
2547
+ const rebuilt = rebuildMember(poolName, member, islanded, served, basis.get(member.name));
2519
2548
  warnIfScopedOnly(poolName, member, rebuilt, islanded, scope);
2520
2549
  ports.sharedExternalsRepo.addOrUpdate(member.name, rebuilt, scope);
2521
2550
  }
2551
+ return true;
2522
2552
  }
2523
2553
  function assignServingBuilds(members, islanded, consumed, hosts, views) {
2524
2554
  for (; ; ) {
@@ -2582,7 +2612,7 @@ function createPoolSharedExternals(config, ports) {
2582
2612
  `[${scope}][pool:${poolName}] '${member.name}' is scoped-only \u2014 no coherent shared build provides it; ${consumers} remotes download their own copy.`
2583
2613
  );
2584
2614
  }
2585
- function rebuildMember(member, islanded, served, basis) {
2615
+ function rebuildMember(poolName, member, islanded, served, basis) {
2586
2616
  let scoped = [];
2587
2617
  let clean = [];
2588
2618
  for (const version of member.external.versions) {
@@ -2602,11 +2632,14 @@ function createPoolSharedExternals(config, ports) {
2602
2632
  const build = served.get(entry.remote)?.get(member.name);
2603
2633
  if (build !== void 0) entry.meta.servedBy = build;
2604
2634
  else delete entry.meta.servedBy;
2635
+ delete entry.meta.poolCause;
2605
2636
  }
2606
2637
  if (basis === void 0) {
2607
2638
  scoped = [...scoped, ...clean.filter((e) => e.meta.servedBy === void 0)];
2608
2639
  clean = clean.filter((e) => e.meta.servedBy !== void 0).map((e) => ({ ...e, action: "skip" }));
2609
2640
  }
2641
+ for (const entry of scoped)
2642
+ entry.meta.poolCause = islanded.get(entry.remote)?.kind ?? "unshared";
2610
2643
  const shareEntries = clean.filter((e) => e.action === "share").sort((a, b) => Number(b.remote === basis) - Number(a.remote === basis));
2611
2644
  const shareVersion = shareEntries.length > 0 ? [
2612
2645
  {
@@ -2632,6 +2665,7 @@ function createPoolSharedExternals(config, ports) {
2632
2665
  };
2633
2666
  return {
2634
2667
  dirty: false,
2668
+ poolName,
2635
2669
  versions: [
2636
2670
  ...shareVersion,
2637
2671
  ...byTag(
@@ -2657,14 +2691,27 @@ function islandWarning(remote, cause, scoped) {
2657
2691
  }
2658
2692
 
2659
2693
  // src/lib/core/2.app/steps/pooling/mark-pools-for-reelection.ts
2694
+ function clearPoolState(external) {
2695
+ let cleared = external.poolName !== void 0;
2696
+ delete external.poolName;
2697
+ for (const version of external.versions)
2698
+ for (const meta of version.remotes) {
2699
+ if (meta.servedBy !== void 0 || meta.poolCause !== void 0) cleared = true;
2700
+ delete meta.servedBy;
2701
+ delete meta.poolCause;
2702
+ }
2703
+ return cleared;
2704
+ }
2660
2705
  function createMarkPoolsForReelection(config, ports) {
2661
2706
  return () => {
2662
- const { useAutoExternalPooling, scopes } = poolableScopes(config, ports.sharedExternalsRepo);
2663
- for (const scope of scopes) {
2707
+ for (const scope of poolableScopes(ports.sharedExternalsRepo)) {
2664
2708
  const sharedExternals = ports.sharedExternalsRepo.getFromScope(scope);
2665
2709
  if (!Object.values(sharedExternals).some((external) => external.dirty)) continue;
2666
2710
  let spread = 0;
2667
- for (const [, members] of buildPools(sharedExternals, useAutoExternalPooling)) {
2711
+ let unpooled = 0;
2712
+ const pooled = /* @__PURE__ */ new Set();
2713
+ for (const [, members] of buildPools(sharedExternals)) {
2714
+ for (const member of members) pooled.add(member.name);
2668
2715
  if (!members.some((m) => m.external.dirty)) continue;
2669
2716
  for (const member of members)
2670
2717
  if (!member.external.dirty) {
@@ -2672,14 +2719,24 @@ function createMarkPoolsForReelection(config, ports) {
2672
2719
  spread++;
2673
2720
  }
2674
2721
  }
2722
+ for (const [name, external] of Object.entries(sharedExternals)) {
2723
+ if (pooled.has(name) || !clearPoolState(external)) continue;
2724
+ external.dirty = true;
2725
+ unpooled++;
2726
+ }
2675
2727
  if (spread > 0)
2676
2728
  config.log.debug(3, `[${scope}] ${spread} pool member(s) marked dirty for re-election.`);
2729
+ if (unpooled > 0)
2730
+ config.log.debug(
2731
+ 3,
2732
+ `[${scope}] ${unpooled} external(s) left every pool; cleared their pool state for re-election.`
2733
+ );
2677
2734
  }
2678
2735
  return Promise.resolve();
2679
2736
  };
2680
2737
  }
2681
2738
 
2682
- // node_modules/.pnpm/@softarc+native-federation@4.4.0_typescript@6.0.3/node_modules/@softarc/native-federation/dist/lib/domain/core/build-notification-options.contract.js
2739
+ // node_modules/.pnpm/@softarc+native-federation@4.7.0/node_modules/@softarc/native-federation/dist/lib/domain/core/build-notification-options.contract.js
2683
2740
  var BuildNotificationType = /* @__PURE__ */ ((BuildNotificationType2) => {
2684
2741
  BuildNotificationType2["COMPLETED"] = "federation-rebuild-complete";
2685
2742
  BuildNotificationType2["ERROR"] = "federation-rebuild-error";
@@ -2687,7 +2744,7 @@ var BuildNotificationType = /* @__PURE__ */ ((BuildNotificationType2) => {
2687
2744
  return BuildNotificationType2;
2688
2745
  })(BuildNotificationType || {});
2689
2746
 
2690
- // node_modules/.pnpm/@softarc+native-federation@4.4.0_typescript@6.0.3/node_modules/@softarc/native-federation/dist/lib/domain/core/chunk.js
2747
+ // node_modules/.pnpm/@softarc+native-federation@4.7.0/node_modules/@softarc/native-federation/dist/lib/domain/core/chunk.js
2691
2748
  var CHUNK_PREFIX = "@nf-internal";
2692
2749
  function toChunkImport(fileName) {
2693
2750
  if (fileName.startsWith("./")) {
@@ -3331,7 +3388,6 @@ function createUpdateCache(config, ports) {
3331
3388
  // src/lib/core/2.app/steps/pooling/pool-dynamic-externals.ts
3332
3389
  function createPoolDynamicExternals(config, ports) {
3333
3390
  return ({ entry, actions }) => {
3334
- const { useAutoExternalPooling } = config.feature;
3335
3391
  const declared = /* @__PURE__ */ new Map();
3336
3392
  for (const external of entry.shared ?? []) {
3337
3393
  const name = external.packageName;
@@ -3343,18 +3399,22 @@ function createPoolDynamicExternals(config, ports) {
3343
3399
  names.add(name);
3344
3400
  }
3345
3401
  if (declared.size === 0) return Promise.resolve({ entry, actions });
3346
- const scope = (name) => {
3402
+ let verdicts = /* @__PURE__ */ new Map();
3403
+ const scope = (name, cause) => {
3347
3404
  actions[name].action = "scope";
3348
3405
  delete actions[name].override;
3406
+ verdicts.set(name, { cause });
3349
3407
  };
3350
3408
  for (const [shareScope, names] of declared) {
3351
- if (!useAutoExternalPooling && !ports.sharedExternalsRepo.hasPoolTag(shareScope)) continue;
3409
+ if (!ports.sharedExternalsRepo.hasPoolState(shareScope)) continue;
3352
3410
  const committed = ports.sharedExternalsRepo.getFromScope(shareScope);
3353
- for (const pool of buildPools(committed, useAutoExternalPooling).values()) {
3411
+ const pools = buildPools(committed);
3412
+ verdicts = /* @__PURE__ */ new Map();
3413
+ for (const pool of pools.values()) {
3354
3414
  const mine = pool.filter((member) => names.has(member.name));
3355
3415
  if (mine.length === 0) continue;
3356
3416
  if (mine.some((member) => actions[member.name].action === "scope")) {
3357
- mine.forEach((member) => scope(member.name));
3417
+ mine.forEach((member) => scope(member.name, "incompatible"));
3358
3418
  continue;
3359
3419
  }
3360
3420
  const asked = gateViews(entry.name, pool);
@@ -3367,11 +3427,18 @@ function createPoolDynamicExternals(config, ports) {
3367
3427
  8,
3368
3428
  `[${shareScope}] '${entry.name}' serves its own family: no committed build offers every entrypoint it imports at a version it accepts \u2014 '${reason.gap}' is the gap, ${closest}. All ${mine.length} members it imports are scoped for it.`
3369
3429
  );
3370
- mine.forEach((member) => scope(member.name));
3430
+ mine.forEach((member) => scope(member.name, "uncovered"));
3371
3431
  continue;
3372
3432
  }
3373
- redirect(entry.name, anchor, pool, asked.view, actions, shareScope);
3433
+ for (const name of redirect(entry.name, anchor, pool, asked.view, actions, shareScope))
3434
+ verdicts.set(name, { servedBy: anchor });
3374
3435
  }
3436
+ const written = {};
3437
+ for (const [name, verdict] of verdicts) {
3438
+ written[name] = recordVerdict(committed[name], entry.name, verdict);
3439
+ ports.sharedExternalsRepo.addOrUpdate(name, written[name], shareScope);
3440
+ }
3441
+ syncPoolNames({ ...committed, ...written }, pools, ports.sharedExternalsRepo, shareScope);
3375
3442
  }
3376
3443
  return Promise.resolve({ entry, actions });
3377
3444
  };
@@ -3436,8 +3503,10 @@ function createPoolDynamicExternals(config, ports) {
3436
3503
  8,
3437
3504
  `[${shareScope}][${remote}] '${anchor}' is not in the cache, so its files cannot be mapped.`
3438
3505
  );
3439
- return;
3506
+ return [];
3440
3507
  }
3508
+ const basis = basisPerMember(pool);
3509
+ const served = [];
3441
3510
  for (const member of pool) {
3442
3511
  const action = actions[member.name];
3443
3512
  if (!action || action.action !== "skip") continue;
@@ -3453,7 +3522,42 @@ function createPoolDynamicExternals(config, ports) {
3453
3522
  }
3454
3523
  action.covered = specifiers;
3455
3524
  if (Object.keys(override).length > 0) action.override = override;
3525
+ if (basis.get(member.name) !== anchor) served.push(member.name);
3526
+ }
3527
+ return served;
3528
+ }
3529
+ function recordVerdict(external, remote, verdict) {
3530
+ if ("servedBy" in verdict) {
3531
+ return {
3532
+ ...external,
3533
+ versions: external.versions.map((v) => ({
3534
+ ...v,
3535
+ remotes: v.remotes.map(
3536
+ (r) => r.name === remote ? { ...r, servedBy: verdict.servedBy } : r
3537
+ )
3538
+ }))
3539
+ };
3540
+ }
3541
+ const moved = [];
3542
+ const versions = external.versions.map((v) => {
3543
+ const own = v.remotes.find((r) => r.name === remote);
3544
+ if (!own) return v;
3545
+ const { servedBy: _anchor, ...rest } = own;
3546
+ const meta = { ...rest, cached: true, poolCause: verdict.cause };
3547
+ if (v.action === "scope")
3548
+ return { ...v, remotes: v.remotes.map((r) => r === own ? meta : r) };
3549
+ moved.push({ tag: v.tag, meta });
3550
+ return { ...v, remotes: v.remotes.filter((r) => r !== own) };
3551
+ }).filter((v) => v.remotes.length > 0);
3552
+ for (const { tag, meta } of moved) {
3553
+ const at = versions.findIndex((v) => v.tag === tag && v.action === "scope");
3554
+ if (at >= 0) versions[at] = { ...versions[at], remotes: [...versions[at].remotes, meta] };
3555
+ else versions.push({ tag, action: "scope", host: false, remotes: [meta] });
3456
3556
  }
3557
+ return {
3558
+ ...external,
3559
+ versions: versions.sort((a, b) => ports.versionCheck.compare(b.tag, a.tag))
3560
+ };
3457
3561
  }
3458
3562
  }
3459
3563
 
@@ -3732,7 +3836,7 @@ var createManifestProvider = () => {
3732
3836
  };
3733
3837
  };
3734
3838
 
3735
- // node_modules/.pnpm/@softarc+native-federation@4.4.0_typescript@6.0.3/node_modules/@softarc/native-federation/dist/lib/utils/normalize.js
3839
+ // node_modules/.pnpm/@softarc+native-federation@4.7.0/node_modules/@softarc/native-federation/dist/lib/utils/normalize.js
3736
3840
  function inferPackageFromSecondary(secondary) {
3737
3841
  const parts = secondary.split("/");
3738
3842
  if (secondary.startsWith("@") && parts.length >= 2) {
@@ -3741,7 +3845,7 @@ function inferPackageFromSecondary(secondary) {
3741
3845
  return parts[0];
3742
3846
  }
3743
3847
 
3744
- // node_modules/.pnpm/@softarc+native-federation@4.4.0_typescript@6.0.3/node_modules/@softarc/native-federation/dist/lib/core/output/densify-externals.js
3848
+ // node_modules/.pnpm/@softarc+native-federation@4.7.0/node_modules/@softarc/native-federation/dist/lib/core/output/densify-externals.js
3745
3849
  function isDense(entry) {
3746
3850
  return "entries" in entry;
3747
3851
  }
@@ -3757,37 +3861,26 @@ function densifyExternals(shared) {
3757
3861
  continue;
3758
3862
  }
3759
3863
  if (isFlatChunk(entry)) {
3760
- const { outFileName, ...rest } = entry;
3761
- result.push({ ...rest, entries: { [entry.packageName]: outFileName } });
3864
+ const { outFileName: outFileName2, ...rest } = entry;
3865
+ result.push({ ...rest, entries: { [entry.packageName]: outFileName2 } });
3762
3866
  continue;
3763
3867
  }
3764
- const parent = inferPackageFromSecondary(entry.packageName);
3765
- const sig = JSON.stringify({
3766
- singleton: entry.singleton,
3767
- strictVersion: entry.strictVersion,
3768
- requiredVersion: entry.requiredVersion,
3769
- version: entry.version,
3770
- shareScope: entry.shareScope
3771
- });
3868
+ const { outFileName, packageName, dev, ...meta } = entry;
3869
+ const parent = inferPackageFromSecondary(packageName);
3870
+ const sig = JSON.stringify(meta, Object.keys(meta).sort());
3772
3871
  const key = parent + " " + sig;
3773
3872
  const existing = groupIndex.get(key);
3774
3873
  if (existing === void 0) {
3775
3874
  const dense = {
3776
- singleton: entry.singleton,
3777
- strictVersion: entry.strictVersion,
3778
- requiredVersion: entry.requiredVersion,
3875
+ ...meta,
3779
3876
  packageName: parent,
3780
- entries: { [entry.packageName]: entry.outFileName }
3877
+ entries: { [packageName]: outFileName }
3781
3878
  };
3782
- if (entry.version !== void 0) dense.version = entry.version;
3783
- if (entry.shareScope !== void 0) dense.shareScope = entry.shareScope;
3784
- if (entry.bundle !== void 0) dense.bundle = entry.bundle;
3785
- if (entry.pool !== void 0) dense.pool = entry.pool;
3786
- if (entry.dev !== void 0) dense.dev = entry.dev;
3879
+ if (dev !== void 0) dense.dev = dev;
3787
3880
  groupIndex.set(key, result.length);
3788
3881
  result.push(dense);
3789
3882
  } else {
3790
- result[existing].entries[entry.packageName] = entry.outFileName;
3883
+ result[existing].entries[packageName] = outFileName;
3791
3884
  }
3792
3885
  }
3793
3886
  return result;
@@ -3837,9 +3930,17 @@ var createRemoteEntryProvider = (config) => {
3837
3930
  };
3838
3931
  };
3839
3932
 
3933
+ // src/lib/core/2.app/config/storage.contract.ts
3934
+ var STORAGE_KEYS = {
3935
+ remotes: "remotes",
3936
+ sharedExternals: "shared-externals",
3937
+ scopedExternals: "scoped-externals",
3938
+ sharedChunks: "shared-chunks"
3939
+ };
3940
+
3840
3941
  // src/lib/core/3.adapters/storage/remote-info.repository.ts
3841
3942
  var createRemoteInfoRepository = (config) => {
3842
- const STORAGE = config.storage("remotes", {});
3943
+ const STORAGE = config.storage(STORAGE_KEYS.remotes, {});
3843
3944
  if (config.clearStorage) STORAGE.clear();
3844
3945
  const _cache = STORAGE.get() ?? {};
3845
3946
  let _dirty = false;
@@ -3881,7 +3982,7 @@ var createRemoteInfoRepository = (config) => {
3881
3982
 
3882
3983
  // src/lib/core/3.adapters/storage/scoped-externals.repository.ts
3883
3984
  var createScopedExternalsRepository = (config) => {
3884
- const STORAGE = config.storage("scoped-externals", {});
3985
+ const STORAGE = config.storage(STORAGE_KEYS.scopedExternals, {});
3885
3986
  if (config.clearStorage) STORAGE.clear();
3886
3987
  const _cache = STORAGE.get() ?? {};
3887
3988
  let _dirty = false;
@@ -3917,7 +4018,7 @@ var createScopedExternalsRepository = (config) => {
3917
4018
  // src/lib/core/3.adapters/storage/shared-externals.repository.ts
3918
4019
  var createSharedExternalsRepository = (config) => {
3919
4020
  const STORAGE = config.storage(
3920
- "shared-externals",
4021
+ STORAGE_KEYS.sharedExternals,
3921
4022
  { [GLOBAL_SCOPE]: {} }
3922
4023
  );
3923
4024
  if (config.clearStorage) STORAGE.clear();
@@ -3927,12 +4028,17 @@ var createSharedExternalsRepository = (config) => {
3927
4028
  // Read from the cache rather than remembered from this init's entries: a warm init may not refetch
3928
4029
  // the tagged remote at all, and pooling has to coordinate its pool anyway. Exits on the first hit.
3929
4030
  // Per share scope, because a pool never spans one: a tag in another scope is no reason to pool here.
3930
- hasPoolTag: function(shareScope) {
4031
+ // Stored pool results count too, since a scope whose last tag left still has pool state to clear.
4032
+ hasPoolState: function(shareScope) {
3931
4033
  const scope = _cache[shareScope ?? GLOBAL_SCOPE];
3932
4034
  if (!scope) return false;
3933
- for (const external of Object.values(scope))
4035
+ for (const external of Object.values(scope)) {
4036
+ if (external.poolName !== void 0) return true;
3934
4037
  for (const version of external.versions)
3935
- for (const remote of version.remotes) if (remote.pool?.trim()) return true;
4038
+ for (const remote of version.remotes)
4039
+ if (remote.pool?.trim() || remote.servedBy !== void 0 || remote.poolCause !== void 0)
4040
+ return true;
4041
+ }
3936
4042
  return false;
3937
4043
  },
3938
4044
  getFromScope: function(shareScope) {
@@ -4008,34 +4114,115 @@ var createSharedExternalsRepository = (config) => {
4008
4114
  };
4009
4115
 
4010
4116
  // src/lib/core/3.adapters/browser/sse-handler.ts
4117
+ var RELAY_CHANNEL = "native-federation-sse";
4118
+ var LOCK_PREFIX = "native-federation-sse:";
4011
4119
  var createSSEHandler = (config) => {
4012
- const subscribers = [];
4120
+ const subscriptions = /* @__PURE__ */ new Map();
4121
+ const locks = typeof navigator !== "undefined" ? navigator.locks : void 0;
4122
+ const canRelay = !!locks && typeof BroadcastChannel !== "undefined";
4123
+ let relay;
4124
+ let bound = false;
4125
+ function reload(reason) {
4126
+ config.log.debug(0, `[SSE] ${reason}, reloading...`);
4127
+ config.reloadBrowserFn();
4128
+ }
4129
+ function openEventSource(endpoint, subscription) {
4130
+ const eventSource = new EventSource(endpoint);
4131
+ eventSource.onmessage = function(event) {
4132
+ const data = JSON.parse(event.data);
4133
+ if (data.type === BuildNotificationType.COMPLETED) {
4134
+ relay?.postMessage({ endpoint });
4135
+ reload("Rebuild completed");
4136
+ }
4137
+ };
4138
+ eventSource.onerror = function(event) {
4139
+ config.log.error(0, "[SSE] Connection error:", event);
4140
+ if (eventSource.readyState !== eventSource.CLOSED) return;
4141
+ config.log.debug(0, `[SSE] Stream for '${endpoint}' will not reopen, releasing the lock`);
4142
+ disconnect(subscription);
4143
+ };
4144
+ return eventSource;
4145
+ }
4146
+ function connect(endpoint, subscription) {
4147
+ if (!locks) {
4148
+ subscription.source = openEventSource(endpoint, subscription);
4149
+ return;
4150
+ }
4151
+ const abort = new AbortController();
4152
+ const held = new Promise((resolve) => {
4153
+ subscription.releaseLock = resolve;
4154
+ });
4155
+ subscription.abort = abort;
4156
+ locks.request(`${LOCK_PREFIX}${endpoint}`, { signal: abort.signal }, () => {
4157
+ if (abort.signal.aborted) return;
4158
+ config.log.debug(0, `[SSE] Holding the connection for '${endpoint}'`);
4159
+ subscription.source = openEventSource(endpoint, subscription);
4160
+ return held;
4161
+ }).catch(() => {
4162
+ config.log.debug(0, `[SSE] Released the connection for '${endpoint}'`);
4163
+ });
4164
+ }
4165
+ function disconnect(subscription) {
4166
+ subscription.source?.close();
4167
+ subscription.abort?.abort();
4168
+ subscription.releaseLock?.();
4169
+ subscription.source = void 0;
4170
+ subscription.abort = void 0;
4171
+ subscription.releaseLock = void 0;
4172
+ }
4173
+ function onPageHide(event) {
4174
+ if (event.persisted) subscriptions.forEach(disconnect);
4175
+ }
4176
+ function onPageShow(event) {
4177
+ if (event.persisted)
4178
+ subscriptions.forEach((subscription, endpoint) => connect(endpoint, subscription));
4179
+ }
4180
+ function bind() {
4181
+ if (bound) return;
4182
+ bound = true;
4183
+ if (canRelay) {
4184
+ relay = new BroadcastChannel(RELAY_CHANNEL);
4185
+ relay.onmessage = (event) => {
4186
+ if (subscriptions.has(event.data?.endpoint)) reload("Rebuild completed in another tab");
4187
+ };
4188
+ }
4189
+ if (typeof window !== "undefined") {
4190
+ window.addEventListener("pagehide", onPageHide);
4191
+ window.addEventListener("pageshow", onPageShow);
4192
+ }
4193
+ }
4194
+ function unbind() {
4195
+ if (!bound) return;
4196
+ bound = false;
4197
+ relay?.close();
4198
+ relay = void 0;
4199
+ if (typeof window !== "undefined") {
4200
+ window.removeEventListener("pagehide", onPageHide);
4201
+ window.removeEventListener("pageshow", onPageShow);
4202
+ }
4203
+ }
4013
4204
  return {
4014
4205
  watchRemoteBuilds: function(endpoint) {
4015
- const eventSource = new EventSource(endpoint);
4016
- eventSource.onmessage = function(event) {
4017
- const data = JSON.parse(event.data);
4018
- if (data.type === BuildNotificationType.COMPLETED) {
4019
- subscribers.forEach((sub) => sub.close());
4020
- config.log.debug(0, "[SSE] Rebuild completed, reloading...");
4021
- config.reloadBrowserFn();
4022
- }
4023
- };
4024
- eventSource.onerror = function(event) {
4025
- config.log.error(0, "[SSE] Connection error:", event);
4026
- };
4027
- subscribers.push(eventSource);
4206
+ if (subscriptions.has(endpoint)) {
4207
+ config.log.debug(0, `[SSE] Already watching '${endpoint}'`);
4208
+ return;
4209
+ }
4210
+ bind();
4211
+ const subscription = {};
4212
+ subscriptions.set(endpoint, subscription);
4213
+ connect(endpoint, subscription);
4028
4214
  },
4029
4215
  closeAll: function() {
4030
- subscribers.forEach((sub) => sub.close());
4031
- subscribers.length = 0;
4216
+ subscriptions.forEach(disconnect);
4217
+ subscriptions.clear();
4218
+ unbind();
4032
4219
  }
4033
4220
  };
4034
4221
  };
4035
4222
 
4036
4223
  // src/lib/core/3.adapters/storage/chunk.repository.ts
4037
4224
  var createChunkRepository = (config) => {
4038
- const STORAGE = config.storage("shared-chunks", {});
4225
+ const STORAGE = config.storage(STORAGE_KEYS.sharedChunks, {});
4039
4226
  if (config.clearStorage) STORAGE.clear();
4040
4227
  const _cache = STORAGE.get() ?? {};
4041
4228
  let _dirty = false;
@@ -4281,12 +4468,55 @@ var globalThisStorageEntry = (namespace) => (key, initialValue) => {
4281
4468
  };
4282
4469
  return entry;
4283
4470
  };
4471
+ globalThisStorageEntry.type = "globalThis";
4472
+ Object.freeze(globalThisStorageEntry);
4473
+
4474
+ // src/lib/core/4.config/storage/publish-storage-info.ts
4475
+ var VERSION = true ? "4.7.0" : "dev";
4476
+ var KEYS = Object.freeze(Object.values(STORAGE_KEYS));
4477
+ var isStorageKey = (key) => KEYS.includes(key);
4478
+ var createStorageInfo = ({
4479
+ type,
4480
+ namespace,
4481
+ storage,
4482
+ exposeGetter
4483
+ }) => {
4484
+ const info = { version: VERSION, type, namespace, keys: KEYS };
4485
+ if (!exposeGetter) return Object.freeze(info);
4486
+ return Object.freeze({
4487
+ ...info,
4488
+ // Cloned again because custom storages may hand out live references.
4489
+ get: (key) => isStorageKey(key) ? cloneEntry(key, storage(key, void 0).get()) : void 0
4490
+ });
4491
+ };
4492
+ var publishStorageInfo = (options) => {
4493
+ try {
4494
+ globalThis.__NF_ORCHESTRATOR__ = Object.freeze({
4495
+ storage: Object.freeze({
4496
+ ...globalThis.__NF_ORCHESTRATOR__?.storage,
4497
+ [options.namespace]: createStorageInfo(options)
4498
+ })
4499
+ });
4500
+ } catch {
4501
+ }
4502
+ };
4284
4503
 
4285
4504
  // src/lib/core/4.config/storage/storage.config.ts
4286
- var createStorageConfig = (override) => ({
4287
- storage: override.storage ? override.storage(override.storageNamespace ?? "__NATIVE_FEDERATION__") : globalThisStorageEntry(override.storageNamespace ?? "__NATIVE_FEDERATION__"),
4288
- clearStorage: override.clearStorage ?? false
4289
- });
4505
+ var createStorageConfig = (override) => {
4506
+ const creator = override.storage ?? globalThisStorageEntry;
4507
+ const namespace = override.storageNamespace ?? "__NATIVE_FEDERATION__";
4508
+ const storage = creator(namespace);
4509
+ publishStorageInfo({
4510
+ type: creator.type ?? "custom",
4511
+ namespace,
4512
+ storage,
4513
+ exposeGetter: override.exposeStorageGetter ?? true
4514
+ });
4515
+ return {
4516
+ storage,
4517
+ clearStorage: override.clearStorage ?? false
4518
+ };
4519
+ };
4290
4520
 
4291
4521
  // src/lib/core/4.config/mode/default.profile.ts
4292
4522
  var defaultProfile = {
@@ -4318,8 +4548,7 @@ var createModeConfig = (override) => {
4318
4548
  strict: strictnessConfig,
4319
4549
  profile: { ...defaultProfile, ...override.profile ?? {} },
4320
4550
  feature: {
4321
- convertFlatSharedInfo: override.feature?.convertFlatSharedInfo ?? false,
4322
- useAutoExternalPooling: override.feature?.useAutoExternalPooling ?? false
4551
+ convertFlatSharedInfo: override.feature?.convertFlatSharedInfo ?? false
4323
4552
  }
4324
4553
  };
4325
4554
  };