4343
4444/**
4545 * JavaScript client for the RemoteFileSource service. Provides bidirectional communication with the server-side
46- * RemoteFileSourceServicePlugin via a message stream.
46+ * RemoteFileSourcePlugin via a message stream.
47+ * <p>
48+ * Events:
49+ * <ul>
50+ * <li>{@link #EVENT_MESSAGE}: Fired for unrecognized messages from the server</li>
51+ * <li>{@link #EVENT_REQUEST_SOURCE}: Fired when the server requests a resource from the client</li>
52+ * </ul>
4753 */
4854@ JsType (namespace = "dh.remotefilesource" , name = "RemoteFileSourceService" )
4955public class JsRemoteFileSourceService extends HasEventHandling {
56+ /** Event name for generic messages from the server */
5057 public static final String EVENT_MESSAGE = "message" ;
58+
59+ /** Event name for resource request events from the server */
5160 public static final String EVENT_REQUEST_SOURCE = "requestsource" ;
5261
62+ // Plugin name must match RemoteFileSourcePlugin.name() on the server
63+ private static final String PLUGIN_NAME = "DeephavenRemoteFileSourcePlugin" ;
64+
65+ // Timeout for setExecutionContext requests (in milliseconds)
66+ private static final int SET_EXECUTION_CONTEXT_TIMEOUT_MS = 30000 ; // 30 seconds
67+
5368 private final JsWidget widget ;
5469
5570 // Track pending setExecutionContext requests
@@ -65,18 +80,17 @@ private JsRemoteFileSourceService(JsWidget widget) {
6580 * Fetches the FlightInfo for the plugin fetch command.
6681 *
6782 * @param connection the worker connection to use
68- * @param pluginName the name of the plugin to fetch
6983 * @return a promise that resolves to the FlightInfo for the plugin fetch
7084 */
7185 @ JsIgnore
72- private static Promise <FlightInfo > fetchPluginFlightInfo (WorkerConnection connection , String pluginName ) {
86+ private static Promise <FlightInfo > fetchPluginFlightInfo (WorkerConnection connection ) {
7387 // Create a new export ticket for the result
7488 Ticket resultTicket = connection .getTickets ().newExportTicket ();
7589
7690 // Create the fetch request
7791 RemoteFileSourcePluginFetchRequest fetchRequest = new RemoteFileSourcePluginFetchRequest ();
7892 fetchRequest .setResultId (resultTicket );
79- fetchRequest .setPluginName (pluginName );
93+ fetchRequest .setPluginName (PLUGIN_NAME );
8094
8195 // Serialize the request to bytes
8296 Uint8Array innerRequestBytes = fetchRequest .serializeBinary ();
@@ -92,8 +106,8 @@ private static Promise<FlightInfo> fetchPluginFlightInfo(WorkerConnection connec
92106 descriptor .setCmd (anyWrappedBytes );
93107
94108 // Send the getFlightInfo request
95- return Callbacks .< FlightInfo , Object > grpcUnaryPromise (
96- c -> connection .flightServiceClient ().getFlightInfo (descriptor , connection .metadata (), c ::apply ));
109+ return Callbacks .grpcUnaryPromise (c ->
110+ connection .flightServiceClient ().getFlightInfo (descriptor , connection .metadata (), c ::apply ));
97111 }
98112
99113 /**
@@ -104,8 +118,7 @@ private static Promise<FlightInfo> fetchPluginFlightInfo(WorkerConnection connec
104118 */
105119 @ JsIgnore
106120 public static Promise <JsRemoteFileSourceService > fetchPlugin (@ TsTypeRef (Object .class ) WorkerConnection connection ) {
107- String pluginName = "DeephavenRemoteFileSourcePlugin" ;
108- return fetchPluginFlightInfo (connection , pluginName )
121+ return fetchPluginFlightInfo (connection )
109122 .then (flightInfo -> {
110123 // The first endpoint contains the ticket for the plugin instance.
111124 // This is the standard Flight pattern: we passed resultTicket in the request,
@@ -124,14 +137,14 @@ public static Promise<JsRemoteFileSourceService> fetchPlugin(@TsTypeRef(Object.c
124137 // The type must match RemoteFileSourcePlugin.name()
125138 TypedTicket typedTicket = new TypedTicket ();
126139 typedTicket .setTicket (dhTicket );
127- typedTicket .setType (pluginName );
140+ typedTicket .setType (PLUGIN_NAME );
128141
129142 JsWidget widget = new JsWidget (connection , typedTicket );
130143
131144 JsRemoteFileSourceService service = new JsRemoteFileSourceService (widget );
132145 return service .connect ();
133146 } else {
134- return Promise .reject ("No endpoints returned from RemoteFileSource plugin fetch" );
147+ return Promise .reject ("No endpoints returned from " + PLUGIN_NAME + " plugin fetch" );
135148 }
136149 });
137150 }
@@ -143,47 +156,83 @@ public static Promise<JsRemoteFileSourceService> fetchPlugin(@TsTypeRef(Object.c
143156 */
144157 @ JsIgnore
145158 private Promise <JsRemoteFileSourceService > connect () {
146- widget .addEventListener ("message" , (Event <WidgetMessageDetails > event ) -> {
147- // Parse the message as RemoteFileSourceServerRequest proto (server→client)
148- Uint8Array payload = event .getDetail ().getDataAsU8 ();
149-
150- try {
151- RemoteFileSourceServerRequest message =
152- RemoteFileSourceServerRequest .deserializeBinary (payload );
153-
154- if (message .hasMetaRequest ()) {
155- // If server has requested a resource from the client, fire request event
156- RemoteFileSourceMetaRequest request = message .getMetaRequest ();
157-
158- DomGlobal .setTimeout (ignore -> fireEvent (EVENT_REQUEST_SOURCE ,
159- new ResourceRequestEvent (message .getRequestId (), request )), 0 );
160- } else if (message .hasSetExecutionContextResponse ()) {
161- // Server acknowledged execution context was set
162- String requestId = message .getRequestId ();
163- Promise .PromiseExecutorCallbackFn .ResolveCallbackFn <Boolean > resolveCallback =
164- pendingSetExecutionContextRequests .remove (requestId );
165- if (resolveCallback != null ) {
166- SetExecutionContextResponse response = message .getSetExecutionContextResponse ();
167- resolveCallback .onInvoke (response .getSuccess ());
168- }
169- } else {
170- // Unknown message type
171- DomGlobal .setTimeout (ignore -> fireEvent (EVENT_MESSAGE , event .getDetail ()), 0 );
172- }
173- } catch (Exception e ) {
174- // Failed to parse as proto, fire generic message event
175- DomGlobal .setTimeout (ignore -> fireEvent (EVENT_MESSAGE , event .getDetail ()), 0 );
176- }
177- });
178-
159+ widget .addEventListener ("message" , this ::handleMessage );
179160 return widget .refetch ().then (w -> Promise .resolve (this ));
180161 }
181162
163+ /**
164+ * Handles incoming messages from the server.
165+ *
166+ * @param event the message event from the server
167+ */
168+ @ JsIgnore
169+ private void handleMessage (Event <WidgetMessageDetails > event ) {
170+ Uint8Array payload = event .getDetail ().getDataAsU8 ();
171+
172+ RemoteFileSourceServerRequest message ;
173+ try {
174+ message = RemoteFileSourceServerRequest .deserializeBinary (payload );
175+ } catch (Exception e ) {
176+ // Failed to parse as proto, fire generic message event
177+ handleUnknownMessage (event );
178+ return ;
179+ }
180+
181+ // Route the parsed message to the appropriate handler
182+ if (message .hasMetaRequest ()) {
183+ handleMetaRequest (message );
184+ } else if (message .hasSetExecutionContextResponse ()) {
185+ handleSetExecutionContextResponse (message );
186+ } else {
187+ handleUnknownMessage (event );
188+ }
189+ }
190+
191+ /**
192+ * Handles a meta request (resource request) from the server.
193+ *
194+ * @param message the server request message
195+ */
196+ @ JsIgnore
197+ private void handleMetaRequest (RemoteFileSourceServerRequest message ) {
198+ RemoteFileSourceMetaRequest request = message .getMetaRequest ();
199+ DomGlobal .setTimeout (ignore -> fireEvent (EVENT_REQUEST_SOURCE ,
200+ new ResourceRequestEvent (message .getRequestId (), request )), 0 );
201+ }
202+
203+ /**
204+ * Handles a set execution context response from the server.
205+ *
206+ * @param message the server request message
207+ */
208+ @ JsIgnore
209+ private void handleSetExecutionContextResponse (RemoteFileSourceServerRequest message ) {
210+ String requestId = message .getRequestId ();
211+ Promise .PromiseExecutorCallbackFn .ResolveCallbackFn <Boolean > resolveCallback =
212+ pendingSetExecutionContextRequests .remove (requestId );
213+ if (resolveCallback != null ) {
214+ SetExecutionContextResponse response = message .getSetExecutionContextResponse ();
215+ resolveCallback .onInvoke (response .getSuccess ());
216+ }
217+ }
218+
219+ /**
220+ * Handles an unknown or unparseable message from the server.
221+ *
222+ * @param event the message event
223+ */
224+ @ JsIgnore
225+ private void handleUnknownMessage (Event <WidgetMessageDetails > event ) {
226+ DomGlobal .setTimeout (ignore -> fireEvent (EVENT_MESSAGE , event .getDetail ()), 0 );
227+ }
228+
182229 /**
183230 * Sets the execution context on the server to identify this message stream as active
184231 * for script execution.
185232 *
186- * @param resourcePaths array of resource paths to resolve from remote source (e.g., ["com/example/Test.groovy", "org/mycompany/Utils.groovy"])
233+ * @param resourcePaths array of resource paths to resolve from remote source
234+ * (e.g., ["com/example/Test.groovy", "org/mycompany/Utils.groovy"]),
235+ * or null/empty for no specific resources
187236 * @return a promise that resolves to true if the server successfully set the execution context, false otherwise
188237 */
189238 @ JsMethod
@@ -195,6 +244,17 @@ public Promise<Boolean> setExecutionContext(@JsOptional String[] resourcePaths)
195244 // Store the resolve callback to call when we get the acknowledgment
196245 pendingSetExecutionContextRequests .put (requestId , resolve );
197246
247+ // Set a timeout to reject the promise if no response is received
248+ DomGlobal .setTimeout (ignore -> {
249+ Promise .PromiseExecutorCallbackFn .ResolveCallbackFn <Boolean > callback =
250+ pendingSetExecutionContextRequests .remove (requestId );
251+ if (callback != null ) {
252+ // Request timed out - reject the promise
253+ reject .onInvoke ("setExecutionContext request timed out after "
254+ + SET_EXECUTION_CONTEXT_TIMEOUT_MS + "ms" );
255+ }
256+ }, SET_EXECUTION_CONTEXT_TIMEOUT_MS );
257+
198258 RemoteFileSourceClientRequest clientRequest = getSetExecutionContextRequest (resourcePaths , requestId );
199259 sendClientRequest (clientRequest );
200260 });
@@ -232,7 +292,8 @@ private void sendClientRequest(RemoteFileSourceClientRequest clientRequest) {
232292 // Serialize the protobuf message to bytes
233293 Uint8Array messageBytes = clientRequest .serializeBinary ();
234294
235- // Send as Uint8Array (which is an ArrayBufferView, compatible with MessageUnion)
295+ // Uint8Array is an ArrayBufferView, which is one of the MessageUnion types
296+ // The unchecked cast is safe because MessageUnion accepts String | ArrayBuffer | ArrayBufferView
236297 widget .sendMessage (Js .uncheckedCast (messageBytes ), null );
237298 }
238299
@@ -271,7 +332,11 @@ public String getResourceName() {
271332 /**
272333 * Responds to this resource request with the given content.
273334 *
274- * @param content the resource content (string or Uint8Array), or null if not found
335+ * @param content the resource content as a String, Uint8Array, or null to indicate
336+ * the resource was not found. If a String is provided, it will be
337+ * UTF-8 encoded before being sent to the server. Uint8Array content
338+ * is sent as-is.
339+ * @throws IllegalArgumentException if content is not a String, Uint8Array, or null
275340 */
276341 @ JsMethod
277342 public void respond (@ JsNullable Object content ) {
0 commit comments