Skip to content

Commit 2e23c29

Browse files
authored
fix(builder): deduplicate concurrent dynamic imports in ecosystem manager (#351)
* build(deps): update @OpenZeppelin/ui-* packages to latest versions Bump ui-components to ^1.4.0, ui-renderer to ^1.1.0, ui-storage to ^1.2.0, ui-types to ^1.11.0 across the builder app, export template, and adapter-evm-core packages. * feat(adapters): add ./networks subpath export for lightweight network loading Each adapter now exposes a `./networks` entry point that re-exports the network configuration without pulling in the full adapter runtime, wallet libraries, or SDK code. This enables address-book and other cross-network features to enumerate networks without breaking lazy loading. * feat(builder): add address book with alias management Integrate the address alias storage plugin from @openzeppelin/ui-storage: - Extend IndexedDB schema (v3) with ALIAS_SCHEMA for alias persistence - Add useAliasStorage hook for alias CRUD operations - Add AliasLabelBridge context for global address label/suggestion resolution and inline edit popover - Add AddressBookDialog with the AddressBookWidget from ui-components - Add Address Book sidebar entry below Templates - Refactor ecosystemManager to use lightweight ./networks subpath imports for cross-network enumeration without loading full adapters - Add useAllNetworks hook using the lightweight getAllNetworks() helper * fix(common): update pnpm lock * fix(builder): add /networks aliases to vitest config and update snapshots Register the new adapter ./networks subpath in vitest's resolver plugin and alias map so export tests can resolve the lightweight network imports. Update versions.ts and export snapshots to match the bumped @OpenZeppelin/ui-* dependency versions. * build(deps): update @OpenZeppelin/ui-* packages to latest published versions Bump ui-types to ^1.11.1, ui-renderer to ^1.1.1, and ui-react (evm-core devDep) to ^1.1.0. Update versions.ts, export template package.json, and export snapshots to match. * fix(builder): use URL constructor for explorer links to preserve query params Solana devnet/testnet explorer URLs contain query strings (e.g. ?cluster=devnet) which were broken by simple string concatenation. Use the URL constructor to properly set the pathname while preserving the search params. * fix(builder): deduplicate concurrent dynamic imports in ecosystem manager Cache in-flight promises instead of resolved values to prevent duplicate imports when multiple callers request the same ecosystem concurrently. Remove permanent failure caching so transient errors can be retried. Simplify getNetworkById by removing redundant cache-only loop.
1 parent d867a56 commit 2e23c29

3 files changed

Lines changed: 97 additions & 70 deletions

File tree

.changeset/bump-evm-core-deps.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@openzeppelin/ui-builder-adapter-evm-core': patch
3+
---
4+
5+
Update `@openzeppelin/ui-types` and `@openzeppelin/ui-components` dependency versions.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@openzeppelin/ui-builder-adapter-evm': minor
3+
'@openzeppelin/ui-builder-adapter-stellar': minor
4+
'@openzeppelin/ui-builder-adapter-polkadot': minor
5+
'@openzeppelin/ui-builder-adapter-solana': minor
6+
'@openzeppelin/ui-builder-adapter-midnight': minor
7+
---
8+
9+
Add `./networks` subpath export for lightweight network loading without pulling in full adapter runtime, wallet libraries, or SDK code. Update `@openzeppelin/ui-components` and `@openzeppelin/ui-types` dependency versions.

apps/builder/src/core/ecosystemManager.ts

Lines changed: 83 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -33,51 +33,60 @@ const ecosystemMetadataRegistry: Record<Ecosystem, EcosystemMetadata> = {
3333
// Full Adapter Module Loading (lazy — static switch required by Vite)
3434
// =============================================================================
3535

36-
const ecosystemCache: Partial<Record<Ecosystem, EcosystemExport>> = {};
36+
const adapterPromiseCache: Partial<Record<Ecosystem, Promise<EcosystemExport>>> = {};
3737

3838
/**
3939
* Loads the full adapter module (networks, createAdapter, adapterConfig).
4040
* This is the "heavy" import — only called when the adapter is actually needed.
41+
* Caches the in-flight promise to deduplicate concurrent calls and clears the
42+
* cache entry on failure so transient errors can be retried.
4143
*/
4244
async function loadAdapterModule(ecosystem: Ecosystem): Promise<EcosystemExport> {
43-
const cached = ecosystemCache[ecosystem];
45+
const cached = adapterPromiseCache[ecosystem];
4446
if (cached) return cached;
4547

46-
let mod: { ecosystemDefinition: EcosystemExport };
47-
switch (ecosystem) {
48-
case 'evm':
49-
mod = await import('@openzeppelin/ui-builder-adapter-evm');
50-
break;
51-
case 'solana':
52-
mod = await import('@openzeppelin/ui-builder-adapter-solana');
53-
break;
54-
case 'stellar':
55-
mod = await import('@openzeppelin/ui-builder-adapter-stellar');
56-
break;
57-
case 'midnight':
58-
mod = await import('@openzeppelin/ui-builder-adapter-midnight');
59-
break;
60-
case 'polkadot':
61-
mod = await import('@openzeppelin/ui-builder-adapter-polkadot');
62-
break;
63-
default: {
64-
const _exhaustiveCheck: never = ecosystem;
65-
throw new Error(
66-
`Adapter package module not defined for ecosystem: ${String(_exhaustiveCheck)}`
67-
);
48+
const promise = (async (): Promise<EcosystemExport> => {
49+
let mod: { ecosystemDefinition: EcosystemExport };
50+
switch (ecosystem) {
51+
case 'evm':
52+
mod = await import('@openzeppelin/ui-builder-adapter-evm');
53+
break;
54+
case 'solana':
55+
mod = await import('@openzeppelin/ui-builder-adapter-solana');
56+
break;
57+
case 'stellar':
58+
mod = await import('@openzeppelin/ui-builder-adapter-stellar');
59+
break;
60+
case 'midnight':
61+
mod = await import('@openzeppelin/ui-builder-adapter-midnight');
62+
break;
63+
case 'polkadot':
64+
mod = await import('@openzeppelin/ui-builder-adapter-polkadot');
65+
break;
66+
default: {
67+
const _exhaustiveCheck: never = ecosystem;
68+
throw new Error(
69+
`Adapter package module not defined for ecosystem: ${String(_exhaustiveCheck)}`
70+
);
71+
}
6872
}
69-
}
73+
return mod.ecosystemDefinition;
74+
})();
75+
76+
adapterPromiseCache[ecosystem] = promise;
77+
promise.catch(() => {
78+
delete adapterPromiseCache[ecosystem];
79+
});
7080

71-
const def = mod.ecosystemDefinition;
72-
ecosystemCache[ecosystem] = def;
73-
return def;
81+
return promise;
7482
}
7583

7684
// =============================================================================
7785
// Lightweight Network Loading (lazy — only loads network configs, not adapters)
7886
// =============================================================================
7987

8088
const networksByEcosystemCache: Partial<Record<Ecosystem, NetworkConfig[]>> = {};
89+
const networkPromiseCache: Partial<Record<Ecosystem, Promise<NetworkConfig[]>>> = {};
8190

8291
const ALL_ECOSYSTEMS: Ecosystem[] = ['evm', 'solana', 'stellar', 'midnight', 'polkadot'];
8392

@@ -86,36 +95,52 @@ const ALL_ECOSYSTEMS: Ecosystem[] = ['evm', 'solana', 'stellar', 'midnight', 'po
8695
* than `loadAdapterModule` because it imports from the `/networks` subpath,
8796
* which only pulls in static config objects + icons — no adapter runtime,
8897
* wallet libraries, or SDK code.
98+
*
99+
* Caches the in-flight promise to deduplicate concurrent calls. Resolved
100+
* values are stored in `networksByEcosystemCache` for synchronous lookups.
101+
* On failure the promise cache entry is cleared so the next call retries.
89102
*/
90103
async function loadNetworksModule(ecosystem: Ecosystem): Promise<NetworkConfig[]> {
91-
const cached = networksByEcosystemCache[ecosystem];
92-
if (cached) return cached;
104+
const resolvedCache = networksByEcosystemCache[ecosystem];
105+
if (resolvedCache) return resolvedCache;
106+
107+
const inflight = networkPromiseCache[ecosystem];
108+
if (inflight) return inflight;
93109

94-
let mod: { networks: NetworkConfig[] };
95-
switch (ecosystem) {
96-
case 'evm':
97-
mod = await import('@openzeppelin/ui-builder-adapter-evm/networks');
98-
break;
99-
case 'solana':
100-
mod = await import('@openzeppelin/ui-builder-adapter-solana/networks');
101-
break;
102-
case 'stellar':
103-
mod = await import('@openzeppelin/ui-builder-adapter-stellar/networks');
104-
break;
105-
case 'midnight':
106-
mod = await import('@openzeppelin/ui-builder-adapter-midnight/networks');
107-
break;
108-
case 'polkadot':
109-
mod = await import('@openzeppelin/ui-builder-adapter-polkadot/networks');
110-
break;
111-
default: {
112-
const _exhaustiveCheck: never = ecosystem;
113-
throw new Error(`Networks module not defined for ecosystem: ${String(_exhaustiveCheck)}`);
110+
const promise = (async (): Promise<NetworkConfig[]> => {
111+
let mod: { networks: NetworkConfig[] };
112+
switch (ecosystem) {
113+
case 'evm':
114+
mod = await import('@openzeppelin/ui-builder-adapter-evm/networks');
115+
break;
116+
case 'solana':
117+
mod = await import('@openzeppelin/ui-builder-adapter-solana/networks');
118+
break;
119+
case 'stellar':
120+
mod = await import('@openzeppelin/ui-builder-adapter-stellar/networks');
121+
break;
122+
case 'midnight':
123+
mod = await import('@openzeppelin/ui-builder-adapter-midnight/networks');
124+
break;
125+
case 'polkadot':
126+
mod = await import('@openzeppelin/ui-builder-adapter-polkadot/networks');
127+
break;
128+
default: {
129+
const _exhaustiveCheck: never = ecosystem;
130+
throw new Error(`Networks module not defined for ecosystem: ${String(_exhaustiveCheck)}`);
131+
}
114132
}
115-
}
116133

117-
networksByEcosystemCache[ecosystem] = mod.networks;
118-
return mod.networks;
134+
networksByEcosystemCache[ecosystem] = mod.networks;
135+
return mod.networks;
136+
})();
137+
138+
networkPromiseCache[ecosystem] = promise;
139+
promise.catch(() => {
140+
delete networkPromiseCache[ecosystem];
141+
});
142+
143+
return promise;
119144
}
120145

121146
// =============================================================================
@@ -127,7 +152,6 @@ export async function getNetworksByEcosystem(ecosystem: Ecosystem): Promise<Netw
127152
return await loadNetworksModule(ecosystem);
128153
} catch (error) {
129154
logger.error('EcosystemManager', `Error loading networks for ${ecosystem}:`, error);
130-
networksByEcosystemCache[ecosystem] = [];
131155
return [];
132156
}
133157
}
@@ -151,29 +175,18 @@ export async function getAllNetworks(): Promise<NetworkConfig[]> {
151175
export async function getNetworkById(id: string): Promise<NetworkConfig | undefined> {
152176
logger.info('EcosystemManager(getNetworkById)', `Attempting to get network by ID: '${id}'`);
153177

154-
for (const ecosystemKey of Object.keys(networksByEcosystemCache)) {
155-
const ecosystem = ecosystemKey as Ecosystem;
156-
const cached = networksByEcosystemCache[ecosystem];
157-
if (cached) {
158-
const network = cached.find((n) => n.id === id);
159-
if (network) {
160-
logger.info(
161-
'EcosystemManager(getNetworkById)',
162-
`Network ID '${id}' found in cache for ecosystem: ${ecosystem}.`
163-
);
164-
return network;
165-
}
166-
}
167-
}
168-
169178
for (const ecosystem of ALL_ECOSYSTEMS) {
170179
let networks = networksByEcosystemCache[ecosystem];
171180
if (!networks) {
172181
logger.info(
173182
'EcosystemManager(getNetworkById)',
174183
`Loading networks for ecosystem: ${ecosystem} to find ID '${id}'.`
175184
);
176-
networks = await getNetworksByEcosystem(ecosystem);
185+
try {
186+
networks = await getNetworksByEcosystem(ecosystem);
187+
} catch {
188+
continue;
189+
}
177190
}
178191
const found = networks?.find((n) => n.id === id);
179192
if (found) {

0 commit comments

Comments
 (0)