@@ -23,11 +23,7 @@ import { simulationState, resetSimulation } from '$lib/pyodide/bridge';
2323import {
2424 collectRequiredToolboxes ,
2525 findMissingRequirements ,
26- performInstall ,
27- discoverToolbox ,
28- registerToolbox ,
29- upsertToolbox ,
30- getCatalogEntry
26+ installAndRegisterToolbox
3127} from '$lib/toolbox' ;
3228import { getCachedPathsimVersion } from '$lib/toolbox/pathsimVersion' ;
3329import type { ToolboxRequirement } from '$lib/types/schema' ;
@@ -197,33 +193,13 @@ async function installRequiredToolboxes(reqs: ToolboxRequirement[]): Promise<voi
197193
198194 for ( const req of missing ) {
199195 try {
200- const installResult = await performInstall ( req . source , req . importPath || undefined ) ;
201- const updated : ToolboxRequirement = {
202- ...req ,
203- importPath : installResult . importPath
204- } ;
205- const discovered = await discoverToolbox ( {
206- importPath : updated . importPath ,
207- eventsImportPath : updated . eventsImportPath
208- } ) ;
209- const catalog = getCatalogEntry ( req . id ) ;
210- const config = {
196+ await installAndRegisterToolbox ( {
211197 id : req . id ,
212198 displayName : req . displayName ,
213199 source : req . source ,
214- importPath : updated . importPath ,
215- eventsImportPath : updated . eventsImportPath ,
216- installedVersion : installResult . installedVersion ,
217- blocks : discovered . blocks . map ( ( b ) => ( { className : b . className , enabled : true } ) ) ,
218- events : discovered . events . map ( ( e ) => ( { className : e . className , enabled : true } ) )
219- } ;
220- registerToolbox ( config , {
221- blocks : discovered . blocks ,
222- events : discovered . events ,
223- defaultCategory : catalog ?. defaultCategory ,
224- categoryByClass : catalog ?. categoryByClass
200+ importPath : req . importPath || undefined ,
201+ eventsImportPath : req . eventsImportPath
225202 } ) ;
226- upsertToolbox ( config ) ;
227203 consoleStore . info ( `[toolbox] installed ${ req . displayName } ` ) ;
228204 } catch ( e ) {
229205 const msg = e instanceof Error ? e . message : String ( e ) ;
@@ -235,7 +211,10 @@ async function installRequiredToolboxes(reqs: ToolboxRequirement[]): Promise<voi
235211/**
236212 * Load a GraphFile into the application state
237213 */
238- export async function loadGraphFile ( file : GraphFile ) : Promise < void > {
214+ export async function loadGraphFile (
215+ file : GraphFile ,
216+ options : { deferToolboxInstall ?: boolean ; backendReady ?: Promise < unknown > } = { }
217+ ) : Promise < void > {
239218 // Migrate old format if needed
240219 file = migrateGraphFile ( file ) ;
241220 // Validate version
@@ -246,36 +225,24 @@ export async function loadGraphFile(file: GraphFile): Promise<void> {
246225 // Reset simulation state (stops running simulation, clears results and Python state)
247226 resetSimulation ( ) ; // Fire and forget - synchronous part stops immediately
248227
249- // Install any runtime toolboxes the file declared as required. Files
250- // saved before this field existed simply skip this step.
251- if ( file . requiredToolboxes && file . requiredToolboxes . length > 0 ) {
252- await installRequiredToolboxes ( file . requiredToolboxes ) ;
253- }
254-
255228 // Clear previous state and wait for UI to update
256229 // This ensures FlowCanvas sees empty state before new data arrives
257230 graphStore . clear ( ) ;
258231 eventStore . clear ( ) ;
259232 consoleStore . clear ( ) ;
260233 await tick ( ) ;
261234
262- // Load graph (including annotations)
235+ // Load graph (including annotations) — happens before toolbox install so
236+ // the user sees the model immediately. Blocks whose toolbox isn't yet
237+ // registered render as (missing) placeholders and upgrade themselves
238+ // reactively via `registryVersion` once `installRequiredToolboxes`
239+ // (below) completes.
263240 graphStore . fromJSON (
264241 file . graph ?. nodes || [ ] ,
265242 file . graph ?. connections || [ ] ,
266243 file . graph ?. annotations || [ ]
267244 ) ;
268245
269- // Surface any block types that ended up unregistered after the install
270- // step (either because the user skipped install, or because the file
271- // has no requiredToolboxes block list — old files / hand-edited files).
272- const unknownTypes = validateNodeTypes ( file . graph ?. nodes || [ ] ) ;
273- if ( unknownTypes . length > 0 ) {
274- consoleStore . warn (
275- `[toolbox] unknown block types in this file: ${ unknownTypes . join ( ', ' ) } . They will render as placeholders.`
276- ) ;
277- }
278-
279246 // Load events
280247 if ( file . events && file . events . length > 0 ) {
281248 eventStore . fromJSON ( file . events ) ;
@@ -317,6 +284,37 @@ export async function loadGraphFile(file: GraphFile): Promise<void> {
317284
318285 // Trigger assembly animation for loaded graph
319286 requestAssemblyAnimation ( ) ;
287+
288+ // Install runtime toolboxes the file declared as required, then surface
289+ // any block types that remain unregistered (user skipped install, or file
290+ // has no requiredToolboxes — old / hand-edited files). In defer mode this
291+ // runs in the background after `backendReady` resolves, so the graph
292+ // shows up before Pyodide is even initialised.
293+ const installAndWarn = async ( ) : Promise < void > => {
294+ if ( file . requiredToolboxes && file . requiredToolboxes . length > 0 ) {
295+ await installRequiredToolboxes ( file . requiredToolboxes ) ;
296+ }
297+ const unknownTypes = validateNodeTypes ( file . graph ?. nodes || [ ] ) ;
298+ if ( unknownTypes . length > 0 ) {
299+ consoleStore . warn (
300+ `[toolbox] unknown block types in this file: ${ unknownTypes . join ( ', ' ) } . They will render as placeholders.`
301+ ) ;
302+ }
303+ } ;
304+
305+ if ( options . deferToolboxInstall ) {
306+ void ( async ( ) => {
307+ try {
308+ if ( options . backendReady ) await options . backendReady ;
309+ await installAndWarn ( ) ;
310+ } catch ( e ) {
311+ const msg = e instanceof Error ? e . message : String ( e ) ;
312+ consoleStore . error ( `[toolbox] deferred install failed: ${ msg } ` ) ;
313+ }
314+ } ) ( ) ;
315+ } else {
316+ await installAndWarn ( ) ;
317+ }
320318}
321319
322320/**
@@ -504,6 +502,14 @@ export interface ImportOptions {
504502 position ?: Position ; // Where to place components (ignored for models)
505503 fileHandle ?: FileSystemFileHandle ; // For native file picker (enables Save)
506504 fileName ?: string ; // Display name (for URL imports or fallback)
505+ // When true, the toolbox install step (which requires Pyodide) is fired
506+ // off in the background — the graph fills immediately and (missing)
507+ // blocks upgrade themselves via `registryVersion` once their toolbox
508+ // registers. Used by the URL-param load on app start, where Pyodide may
509+ // still be initialising. The deferred install awaits `backendReady`
510+ // first, so it's safe to pass a not-yet-ready promise.
511+ deferToolboxInstall ?: boolean ;
512+ backendReady ?: Promise < unknown > ;
507513}
508514
509515/**
@@ -659,7 +665,10 @@ async function importModel(
659665 simulationSettings : content . simulationSettings || INITIAL_SIMULATION_SETTINGS
660666 } ;
661667
662- await loadGraphFile ( graphFile ) ;
668+ await loadGraphFile ( graphFile , {
669+ deferToolboxInstall : options . deferToolboxInstall ,
670+ backendReady : options . backendReady
671+ } ) ;
663672
664673 // Update current file tracking
665674 currentFileHandle = options . fileHandle || null ;
0 commit comments