Skip to content

Commit df05a62

Browse files
committed
Cleanup JsRemoteFileSourceService (#DH-20578)
1 parent 77ff6b3 commit df05a62

File tree

1 file changed

+111
-46
lines changed

1 file changed

+111
-46
lines changed

web/client-api/src/main/java/io/deephaven/web/client/api/remotefilesource/JsRemoteFileSourceService.java

Lines changed: 111 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,28 @@
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")
4955
public 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

Comments
 (0)