Skip to content

Commit 15551df

Browse files
authored
feat: JS API reconnect (#1149)
- Listen for `EVENT_DISCONNECT` and `EVENT_RECONNECT` on the connection, and display a "Reconnecting..." message in the console. - No longer listen to the `HACK_CONNECTION_FAILURE` (it's been deprecated). - Listen for the `SHUTDOWN` event and display a message after shutdown. - Fixes #1140 Testing steps: 1. Start up server with deephaven-core JS API reconnect changes: deephaven/deephaven-core#3502 2. Use ngrok to start a tunnel to that port, e.g.: `ngrok http 10000` 3. Start up Web UI connecting to that tunnel, e.g.: `VITE_CORE_API_URL=http://acfc-23-233-0-34.ngrok.io/jsapi npm start` 4. Open the Web UI in Firefox, run some commands to make sure initial connection is fine. 5. Press Alt and from the File menu, select "Work Offline" 6. See it transition to a disconnected state. Disconnected message should appear and should not be able to enter new commands 7. Reconnect by deselecting "Work Offline" option from Step 5 8. See it transition to connected state. Should be able to run commands and have the results appear. 9. Kill the server (Ctrl+C). See it transition to a Shutdown state, app unloaded.
1 parent 1e4f8f9 commit 15551df

12 files changed

Lines changed: 435 additions & 73 deletions

File tree

__mocks__/dh-core.js

Lines changed: 127 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,20 @@ class DeephavenObject {
426426
callbacks[i](event);
427427
}
428428
}
429+
430+
nextEvent(name, timeout) {
431+
let cleanup;
432+
return new Promise((resolve, reject) => {
433+
cleanup = this.addEventListener(name, detail => {
434+
resolve(detail);
435+
cleanup();
436+
});
437+
});
438+
}
439+
440+
hasListeners(name) {
441+
return (this._listeners[name]?.length ?? 0) > 0;
442+
}
429443
}
430444

431445
class Sort {
@@ -615,6 +629,11 @@ class Table extends DeephavenObject {
615629
size = ROW_COUNT,
616630
suppressFilter = false,
617631
customColumns = [],
632+
description = 'Mock Table',
633+
layoutHints = null,
634+
hasInputTable = false,
635+
pluginName = null,
636+
totalsTableConfig = {},
618637
} = {}) {
619638
super({ sort, filter, columns, size });
620639

@@ -623,6 +642,7 @@ class Table extends DeephavenObject {
623642
this.columns = columns;
624643
this.customColumns = customColumns;
625644
this.size = size;
645+
this.totalSize = size;
626646
this.suppressFilter = suppressFilter;
627647

628648
this.startRow = 0;
@@ -631,6 +651,11 @@ class Table extends DeephavenObject {
631651
this.pendingViewportUpdate = false;
632652
this.isClosed = false;
633653
this.isUncoalesced = false;
654+
this.description = description;
655+
this.layoutHints = layoutHints;
656+
this.hasInputTable = hasInputTable;
657+
this.pluginName = pluginName;
658+
this.totalsTableConfig = totalsTableConfig;
634659
}
635660

636661
close() {}
@@ -665,7 +690,7 @@ class Table extends DeephavenObject {
665690
const { startRow, endRow, viewportColumns, size, filter } = this;
666691

667692
if (startRow == null || endRow == null || viewportColumns == null) {
668-
return null;
693+
throw new Error('Viewport not set');
669694
}
670695

671696
let rows = [];
@@ -686,13 +711,17 @@ class Table extends DeephavenObject {
686711
return viewportData;
687712
}
688713

714+
findColumn(name) {
715+
const column = this.columns.find(col => col.name === name);
716+
if (column === undefined) {
717+
throw new Error(`Column ${name} not found`);
718+
}
719+
return column;
720+
}
721+
689722
findColumns(names) {
690723
return names.map(name => {
691-
const column = this.columns.find(col => col.name === name);
692-
if (column === undefined) {
693-
throw new Error(`Column ${name} not found`);
694-
}
695-
return column;
724+
return this.findColumn(name);
696725
});
697726
}
698727

@@ -744,6 +773,10 @@ class Table extends DeephavenObject {
744773
});
745774
}
746775

776+
getGrandTotalsTable() {
777+
return this.getTotalsTable();
778+
}
779+
747780
selectDistinct() {
748781
return new Promise((resolve, reject) => {
749782
const table = makeDummyTable();
@@ -758,10 +791,46 @@ class Table extends DeephavenObject {
758791
});
759792
}
760793

794+
reverse() {
795+
return this.copy();
796+
}
797+
761798
rollup() {
762799
return this.copy();
763800
}
764801

802+
treeTable() {
803+
return this.copy();
804+
}
805+
806+
inputTable() {
807+
return Promise.resolve(new InputTable());
808+
}
809+
810+
freeze() {
811+
return this.copy();
812+
}
813+
814+
snapshot() {
815+
return this.copy();
816+
}
817+
818+
join() {
819+
return this.copy();
820+
}
821+
822+
byExternal() {
823+
return this.copy();
824+
}
825+
826+
seekRow() {
827+
return Promise.resolve(-1);
828+
}
829+
830+
getColumnStatistics() {
831+
return Promise.reject(new Error('Column statistics not implemented'));
832+
}
833+
765834
subscribe(columns, updateInterval = UPDATE_INTERVAL) {
766835
return new TableSubscription({
767836
table: this,
@@ -783,6 +852,8 @@ Table.EVENT_UPDATED = 'updated';
783852
Table.EVENT_CONNECT = 'connect';
784853
Table.EVENT_DISCONNECT = 'disconnect';
785854
Table.EVENT_RECONNECT = 'reconnect';
855+
Table.EVENT_RECONNECTFAILED = 'reconnectfailed';
856+
Table.SIZE_UNCOALESCED = 'sizeuncoalesced';
786857

787858
class TableViewportSubscription extends DeephavenObject {
788859
constructor({ table = makeDummyTable() } = {}) {
@@ -1267,16 +1338,44 @@ class IdeConnection extends DeephavenObject {
12671338
}
12681339
}
12691340

1341+
IdeConnection.EVENT_DISCONNECT = 'disconnect';
1342+
IdeConnection.EVENT_RECONNECT = 'reconnect';
1343+
12701344
class IdeSession extends DeephavenObject {
12711345
constructor(language) {
12721346
super();
12731347

12741348
this.language = language;
12751349
this.tables = [];
12761350
this.widgets = [];
1351+
this.logMessageCallbacks = [];
1352+
this.subscribeCallbacks = [];
1353+
}
1354+
1355+
onLogMessage(callback) {
1356+
this.logMessageCallbacks.push(callback);
1357+
return () => {
1358+
this.logMessageCallbacks = this.logMessageCallbacks.filter(
1359+
cb => cb !== callback
1360+
);
1361+
};
12771362
}
12781363

1279-
onLogMessage(callback) {}
1364+
subscribeToFieldUpdates(callback) {
1365+
this.subscribeCallbacks.push(callback);
1366+
return () => {
1367+
this.subscribeCallbacks = this.subscribeCallbacks.filter(
1368+
cb => cb !== callback
1369+
);
1370+
};
1371+
}
1372+
1373+
notifySubscribeCallbacks(changes) {
1374+
const callbacks = [...this.subscribeCallbacks];
1375+
callbacks.forEach(cb => {
1376+
cb(changes);
1377+
});
1378+
}
12801379

12811380
close() {}
12821381
/**
@@ -1374,6 +1473,8 @@ class IdeSession extends DeephavenObject {
13741473
}
13751474

13761475
timer = setTimeout(() => {
1476+
this.notifySubscribeCallbacks(tableChanges);
1477+
this.notifySubscribeCallbacks(widgetChanges);
13771478
resolve(result);
13781479
}, delay);
13791480
});
@@ -1413,6 +1514,23 @@ class IdeSession extends DeephavenObject {
14131514
});
14141515
}
14151516

1517+
getTreeTable(name) {
1518+
// We're just going to use a regular table for TreeTable
1519+
// Actual impl is different, but should be fine for mock
1520+
this.getTable(name);
1521+
}
1522+
1523+
getObject(variableDefinition) {
1524+
switch (variableDefinition.type) {
1525+
case dh.VariableType.FIGURE:
1526+
return this.getFigure(variableDefinition.title);
1527+
case dh.VariableType.TreeTable:
1528+
return this.getTreeTable(variableDefinition.title);
1529+
default:
1530+
return this.getTable(variableDefinition.title);
1531+
}
1532+
}
1533+
14161534
openDocument() {}
14171535
changeDocument() {}
14181536
getCompletionItems() {
@@ -1864,6 +1982,8 @@ const dh = {
18641982
TotalsTableConfig: TotalsTableConfig,
18651983
TableViewportSubscription,
18661984
TableSubscription,
1985+
// TreeTable and Table are different in actual implementation, but should be okay for the mock
1986+
TreeTable: Table,
18671987
Column: Column,
18681988
RangeSet,
18691989
Row: Row,

packages/code-studio/src/main/AppInit.tsx

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,10 @@ function AppInit(props: AppInitProps) {
153153
setServerConfigValues,
154154
} = props;
155155

156+
// General error means the app is dead and is unlikely to recover
156157
const [error, setError] = useState<unknown>();
158+
// Disconnect error may be temporary, so just show an error overlaid on the app
159+
const [disconnectError, setDisconnectError] = useState<unknown>();
157160
const [isFontLoading, setIsFontLoading] = useState(true);
158161

159162
const initClient = useCallback(async () => {
@@ -175,14 +178,12 @@ function AppInit(props: AppInitProps) {
175178
? // Fall back to the old API for anonymous auth if the new API is not supported
176179
createConnection()
177180
: coreClient.getAsIdeConnection());
178-
connection.addEventListener(
179-
dh.IdeConnection.HACK_CONNECTION_FAILURE,
180-
event => {
181-
const { detail } = event;
182-
log.error('Connection failure', `${JSON.stringify(detail)}`);
183-
setError(`Unable to connect: ${detail.details ?? 'Unknown Error'}`);
184-
}
185-
);
181+
connection.addEventListener(dh.IdeConnection.EVENT_SHUTDOWN, event => {
182+
const { detail } = event;
183+
log.info('Shutdown', `${JSON.stringify(detail)}`);
184+
setError(`Server shutdown: ${detail ?? 'Unknown reason'}`);
185+
setDisconnectError(null);
186+
});
186187

187188
const sessionWrapper = await loadSessionWrapper(connection);
188189
const name = 'user';
@@ -318,7 +319,10 @@ function AppInit(props: AppInitProps) {
318319

319320
const isLoading = (workspace == null && error == null) || isFontLoading;
320321
const isLoaded = !isLoading && error == null;
321-
const errorMessage = error != null ? `${error}` : null;
322+
const errorMessage =
323+
error != null || disconnectError != null
324+
? `${error ?? disconnectError}`
325+
: null;
322326

323327
return (
324328
<>

0 commit comments

Comments
 (0)