@@ -20,7 +20,6 @@ import { pluginId } from "@psibase/common-lib/messaging/plugin-id";
2020
2121import { AppInterface } from "./app-interface" ;
2222import { CallContext } from "./call-context" ;
23- import { REDIRECT_ERROR_CODE } from "./constants" ;
2423import { getRecoverableError } from "./plugin/errors" ;
2524import { PluginLoader } from "./plugin/plugin-loader" ;
2625import { Plugins } from "./plugin/plugins" ;
@@ -49,6 +48,19 @@ const systemPlugins: Array<QualifiedPluginId> = [
4948 pluginId ( "webcrypto" , "plugin" ) ,
5049] ;
5150
51+ // Control-flow signal thrown to unwind out of a plugin call.
52+ //
53+ // It extends Error so that JCO-transpiled glue never intercepts it: JCO's
54+ // getErrorPayload() re-throws Error instances rather than converting them into a
55+ // component Result::Err. The one exception is an Error carrying its own "payload"
56+ // property, so this class must NOT define one.
57+ class PromptSignal extends Error {
58+ constructor ( readonly kind : "prompt" | "embedded-error" | "preload-error" ) {
59+ super ( `prompt-signal:${ kind } ` ) ;
60+ this . name = "PromptSignal" ;
61+ }
62+ }
63+
5264// The supervisor facilitates all communication
5365export class Supervisor implements AppInterface {
5466 private plugins : Plugins ;
@@ -59,6 +71,8 @@ export class Supervisor implements AppInterface {
5971
6072 private embedder : string | undefined ;
6173
74+ private inPreload = false ;
75+
6276 parser : Promise < any > ;
6377
6478 parentOrigination : OriginationData | undefined ;
@@ -111,55 +125,72 @@ export class Supervisor implements AppInterface {
111125 }
112126
113127 private async doPreload ( plugins : QualifiedPluginId [ ] ) {
114- await chainIdPromise ;
128+ this . inPreload = true ;
129+ try {
130+ await chainIdPromise ;
115131
116- if ( plugins . length === 0 ) {
117- return ;
118- }
132+ if ( plugins . length === 0 ) {
133+ return ;
134+ }
119135
120- // Phase 0: Loads systemPlugins, including those needed to get current user, i.e., accounts, host:auth
121- this . loader . trackPlugins ( [ ...systemPlugins ] ) ;
122- await this . loader . processPlugins ( ) ;
123- await this . loader . awaitReady ( ) ;
136+ // Phase 0: Loads systemPlugins, including those needed to get current user, i.e., accounts, host:auth
137+ this . loader . trackPlugins ( [ ...systemPlugins ] ) ;
138+ await this . loader . processPlugins ( ) ;
139+ await this . loader . awaitReady ( ) ;
124140
125- // Required to instantiate system plugins to execute the plugin calls below
126- await this . plugins . instantiateAll ( ) ;
141+ // Required to instantiate system plugins to execute the plugin calls below
142+ await this . plugins . instantiateAll ( ) ;
127143
128- if ( isEmbedded ) {
129- const promptDetails = await this . supervisorCall (
130- getCallArgs ( "host" , "prompt" , "admin" , "get-active-prompt" , [ ] ) ,
131- ) ;
132- if ( promptDetails ) {
133- this . embedder = promptDetails . activeApp ;
134- delete this . context ; // A new one will be created with the embedder
144+ if ( isEmbedded ) {
145+ const promptDetails = await this . supervisorCall (
146+ getCallArgs (
147+ "host" ,
148+ "prompt" ,
149+ "admin" ,
150+ "get-active-prompt" ,
151+ [ ] ,
152+ ) ,
153+ ) ;
154+ if ( promptDetails ) {
155+ this . embedder = promptDetails . activeApp ;
156+ delete this . context ; // A new one will be created with the embedder
157+ }
135158 }
136- }
137159
138- setQueryToken ( this . getActiveQueryToken ( ) ) ;
139-
140- // Phase 1: Compile app plugins (NO instantiation yet — Memory deferred).
141- // The sync call to getAuthServices below only touches Phase 0 plugins.
142- this . loader . trackPlugins ( [ ...plugins ] ) ;
143- await this . loader . processPlugins ( ) ;
144- await this . loader . awaitReady ( ) ;
160+ setQueryToken ( this . getActiveQueryToken ( ) ) ;
161+
162+ // Phase 1: Compile app plugins (NO instantiation yet — Memory deferred).
163+ // The sync call to getAuthServices below only touches Phase 0 plugins.
164+ this . loader . trackPlugins ( [ ...plugins ] ) ;
165+ await this . loader . processPlugins ( ) ;
166+ await this . loader . awaitReady ( ) ;
167+
168+ // Phase 2: Load the auth services for all connected accounts.
169+ // This sync call uses accounts:plugin (Phase 0, already instantiated).
170+ const auth_services : string [ ] = this . supervisorCall (
171+ getCallArgs (
172+ "accounts" ,
173+ "plugin" ,
174+ "admin" ,
175+ "get-auth-services" ,
176+ [ ] ,
177+ ) ,
178+ ) ;
145179
146- // Phase 2: Load the auth services for all connected accounts.
147- // This sync call uses accounts:plugin (Phase 0, already instantiated).
148- const auth_services : string [ ] = this . supervisorCall (
149- getCallArgs ( "accounts" , "plugin" , "admin" , "get-auth-services" , [ ] ) ,
150- ) ;
180+ const addtl_plugins : QualifiedPluginId [ ] = [ ] ;
181+ for ( const service of auth_services ) {
182+ if ( ! service ) continue ;
151183
152- const addtl_plugins : QualifiedPluginId [ ] = [ ] ;
153- for ( const service of auth_services ) {
154- if ( ! service ) continue ;
184+ // Current limitation: an auth service plugin must be called "plugin" ("<service>:plugin")
185+ addtl_plugins . push ( pluginId ( service , "plugin" ) ) ;
186+ }
187+ this . loader . trackPlugins ( addtl_plugins ) ;
155188
156- // Current limitation: an auth service plugin must be called "plugin" ("<service>:plugin")
157- addtl_plugins . push ( pluginId ( service , "plugin" ) ) ;
189+ await this . loader . processPlugins ( ) ;
190+ await this . loader . awaitReady ( ) ;
191+ } finally {
192+ this . inPreload = false ;
158193 }
159- this . loader . trackPlugins ( addtl_plugins ) ;
160-
161- await this . loader . processPlugins ( ) ;
162- await this . loader . awaitReady ( ) ;
163194 }
164195
165196 private replyToParent ( id : string , result : any ) {
@@ -270,6 +301,13 @@ export class Supervisor implements AppInterface {
270301 ) ;
271302 }
272303
304+ requestPrompt ( ) : never {
305+ if ( this . inPreload ) {
306+ throw new PromptSignal ( "preload-error" ) ;
307+ }
308+ throw new PromptSignal ( isEmbedded ? "embedded-error" : "prompt" ) ;
309+ }
310+
273311 call ( args : QualifiedFunctionCallArgs ) : any {
274312 assertTruthy ( this . context , "Uninitialized call context" ) ;
275313
@@ -431,21 +469,35 @@ export class Supervisor implements AppInterface {
431469 // Send plugin result to parent window
432470 this . replyToParent ( id , result ) ;
433471 } catch ( e ) {
434- const err = getRecoverableError ( e ) ;
435- if ( err ) {
436- let newError ;
437- if ( err . code === REDIRECT_ERROR_CODE ) {
438- newError = new RedirectErrorObject (
439- err . producer ,
440- err . message ,
441- ) ;
442- } else {
443- newError = new PluginErrorObject ( err . producer , err . message ) ;
444- }
445- this . replyToParent ( id , newError ) ;
472+ let result : any ;
473+ if ( e instanceof PromptSignal && e . kind === "prompt" ) {
474+ result = new RedirectErrorObject (
475+ { service : "host" , plugin : "prompt" } ,
476+ "user_prompt_request" ,
477+ ) ;
478+ } else if (
479+ e instanceof PromptSignal &&
480+ e . kind === "embedded-error"
481+ ) {
482+ result = new PluginErrorObject (
483+ { service : "host" , plugin : "prompt" } ,
484+ "Cannot prompt in embedded mode" ,
485+ ) ;
486+ } else if (
487+ e instanceof PromptSignal &&
488+ e . kind === "preload-error"
489+ ) {
490+ result = new PluginErrorObject (
491+ { service : "host" , plugin : "prompt" } ,
492+ "Cannot trigger user prompt during plugin preload" ,
493+ ) ;
446494 } else {
447- this . replyToParent ( id , e ) ;
495+ const err = getRecoverableError ( e ) ;
496+ result = err
497+ ? new PluginErrorObject ( err . producer , err . message )
498+ : e ;
448499 }
500+ this . replyToParent ( id , result ) ;
449501 } finally {
450502 this . plugins . disposeAll ( ) ;
451503 this . cleanupSessionState ( ) ;
0 commit comments