diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/JsTable.java b/web/client-api/src/main/java/io/deephaven/web/client/api/JsTable.java index a10f5cf7808..fcd9f450af7 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/api/JsTable.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/JsTable.java @@ -1419,8 +1419,8 @@ public Promise partitionBy(Object keys, @JsOptional Boolean TypedTicket typedTicket = new TypedTicket(); typedTicket.setType(JsVariableType.PARTITIONEDTABLE); typedTicket.setTicket(partitionedTableTicket); - Promise fetchPromise = - new JsPartitionedTable(workerConnection, new JsWidget(workerConnection, typedTicket)).refetch(); + Promise fetchPromise = new JsWidget(workerConnection, typedTicket).refetch().then( + widget -> Promise.resolve(new JsPartitionedTable(workerConnection, widget))); // Ensure that the partition failure propagates first, but the result of the fetch will be returned - both // are running concurrently. diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/WorkerConnection.java b/web/client-api/src/main/java/io/deephaven/web/client/api/WorkerConnection.java index 7237e5b5a66..f9611a1bc57 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/api/WorkerConnection.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/WorkerConnection.java @@ -49,6 +49,8 @@ import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.object_pb.FetchObjectResponse; import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.object_pb_service.ObjectServiceClient; import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.partitionedtable_pb_service.PartitionedTableServiceClient; +import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.session_pb.ExportRequest; +import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.session_pb.ExportResponse; import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.session_pb.ReleaseRequest; import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.session_pb.TerminationNotificationRequest; import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.session_pb_service.SessionServiceClient; @@ -78,6 +80,7 @@ import io.deephaven.web.client.api.console.JsVariableDefinition; import io.deephaven.web.client.api.console.JsVariableType; import io.deephaven.web.client.api.i18n.JsTimeZone; +import io.deephaven.web.client.api.impl.TicketAndPromise; import io.deephaven.web.client.api.lifecycle.HasLifecycle; import io.deephaven.web.client.api.parse.JsDataHandler; import io.deephaven.web.client.api.state.StateCache; @@ -701,11 +704,12 @@ final class Listener implements Consumer { @Override public void accept(JsVariableChanges changes) { JsVariableDefinition foundField = changes.getCreated() - .find((field, p1, p2) -> field.getTitle().equals(name) && field.getType().equals(type)); + .find((field, p1, p2) -> field.getTitle().equals(name) + && field.getType().equalsIgnoreCase(type)); if (foundField == null) { foundField = changes.getUpdated().find((field, p1, p2) -> field.getTitle().equals(name) - && field.getType().equals(type)); + && field.getType().equalsIgnoreCase(type)); } if (foundField != null) { @@ -756,23 +760,23 @@ public Promise getTable(JsVariableDefinition varDef, @Nullable Boolean } public Promise getObject(JsVariableDefinition definition) { - if (JsVariableType.TABLE.equals(definition.getType())) { + if (JsVariableType.TABLE.equalsIgnoreCase(definition.getType())) { return getTable(definition, null); - } else if (JsVariableType.FIGURE.equals(definition.getType())) { + } else if (JsVariableType.FIGURE.equalsIgnoreCase(definition.getType())) { return getFigure(definition); - } else if (JsVariableType.PANDAS.equals(definition.getType())) { + } else if (JsVariableType.PANDAS.equalsIgnoreCase(definition.getType())) { return getWidget(definition) .then(widget -> widget.getExportedObjects()[0].fetch()); - } else if (JsVariableType.PARTITIONEDTABLE.equals(definition.getType())) { + } else if (JsVariableType.PARTITIONEDTABLE.equalsIgnoreCase(definition.getType())) { return getPartitionedTable(definition); - } else if (JsVariableType.HIERARCHICALTABLE.equals(definition.getType())) { + } else if (JsVariableType.HIERARCHICALTABLE.equalsIgnoreCase(definition.getType())) { return getHierarchicalTable(definition); } else { - if (JsVariableType.TABLEMAP.equals(definition.getType())) { + if (JsVariableType.TABLEMAP.equalsIgnoreCase(definition.getType())) { JsLog.warn( "TableMap is now known as PartitionedTable, fetching as a plain widget. To fetch as a PartitionedTable use that as the type."); } - if (JsVariableType.TREETABLE.equals(definition.getType())) { + if (JsVariableType.TREETABLE.equalsIgnoreCase(definition.getType())) { JsLog.warn( "TreeTable is now HierarchicalTable, fetching as a plain widget. To fetch as a HierarchicalTable use that as this type."); } @@ -886,15 +890,25 @@ public Promise whenServerReady(String operationName) { return Promise.resolve(this); default: // not possible, means null state - // noinspection unchecked - return (Promise) Promise.reject("Can't " + operationName + " while connection is in state " + state); + return Promise.reject("Can't " + operationName + " while connection is in state " + state); } } + private TicketAndPromise exportScopeTicket(JsVariableDefinition varDef) { + Ticket ticket = getConfig().newTicket(); + return new TicketAndPromise<>(ticket, whenServerReady("exportScopeTicket").then(server -> { + ExportRequest req = new ExportRequest(); + req.setSourceId(createTypedTicket(varDef).getTicket()); + req.setResultId(ticket); + return Callbacks.grpcUnaryPromise( + c -> sessionServiceClient().exportFromTicket(req, metadata(), c::apply)); + }), this); + } + public Promise getPartitionedTable(JsVariableDefinition varDef) { return whenServerReady("get a partitioned table") - .then(server -> new JsPartitionedTable(this, new JsWidget(this, createTypedTicket(varDef))) - .refetch()); + .then(server -> getWidget(varDef)) + .then(widget -> new JsPartitionedTable(this, widget).refetch()); } public Promise getTreeTable(JsVariableDefinition varDef) { @@ -906,7 +920,7 @@ public Promise getHierarchicalTable(JsVariableDefinition varDef) { } public Promise getFigure(JsVariableDefinition varDef) { - if (!varDef.getType().equals("Figure")) { + if (!varDef.getType().equalsIgnoreCase("Figure")) { throw new IllegalArgumentException("Can't load as a figure: " + varDef.getType()); } return whenServerReady("get a figure") @@ -935,7 +949,13 @@ private TypedTicket createTypedTicket(JsVariableDefinition varDef) { } public Promise getWidget(JsVariableDefinition varDef) { - return getWidget(createTypedTicket(varDef)); + return exportScopeTicket(varDef) + .race(ticket -> { + TypedTicket typedTicket = new TypedTicket(); + typedTicket.setType(varDef.getType()); + typedTicket.setTicket(ticket); + return getWidget(typedTicket); + }).promise(); } public Promise getWidget(TypedTicket typedTicket) { diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/impl/TicketAndPromise.java b/web/client-api/src/main/java/io/deephaven/web/client/api/impl/TicketAndPromise.java new file mode 100644 index 00000000000..4f850da0a9f --- /dev/null +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/impl/TicketAndPromise.java @@ -0,0 +1,70 @@ +package io.deephaven.web.client.api.impl; + +import elemental2.promise.IThenable; +import elemental2.promise.Promise; +import io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.ticket_pb.Ticket; +import io.deephaven.web.client.api.WorkerConnection; +import io.deephaven.web.shared.fu.JsFunction; + +/** + * Pair of ticket and the promise that indicates it has been resolved. Tickets are usable before they are resolved, but + * to ensure that all operations completed successfully, the promise should be used to handle errors. + */ +public class TicketAndPromise implements IThenable { + private final Ticket ticket; + private final Promise promise; + private final WorkerConnection connection; + private boolean released = false; + + public TicketAndPromise(Ticket ticket, Promise promise, WorkerConnection connection) { + this.ticket = ticket; + this.promise = promise; + this.connection = connection; + } + + public TicketAndPromise(Ticket ticket, WorkerConnection connection) { + this(ticket, (Promise) Promise.resolve(ticket), connection); + } + + public Promise promise() { + return promise; + } + + public Ticket ticket() { + return ticket; + } + + @Override + public TicketAndPromise then(ThenOnFulfilledCallbackFn onFulfilled) { + return new TicketAndPromise<>(ticket, promise.then(onFulfilled), connection); + } + + /** + * Rather than waiting for the original promise to succeed, lets the caller start a new call based only on the + * original ticket. The intent of "race" here is unlike Promise.race(), where the first to succeed should resolve - + * instead, this raced call will be sent to the server even though the previous call has not successfully returned, + * and the server is responsible for ensuring they happen in the correct order. + * + * @param racedCall the call to perform at the same time that any pending call is happening + * @return a new TicketAndPromise that will resolve when all work is successful + * @param type of the next call to perform + */ + public TicketAndPromise race(JsFunction> racedCall) { + IThenable raced = racedCall.apply(ticket); + return new TicketAndPromise<>(ticket, Promise.all(promise, raced).then(ignore -> raced), connection); + } + + @Override + public IThenable then(ThenOnFulfilledCallbackFn onFulfilled, + ThenOnRejectedCallbackFn onRejected) { + return promise.then(onFulfilled, onRejected); + } + + public void release() { + if (!released) { + // don't double-release, in cases where the same ticket is used for multiple parts of the request + released = true; + connection.releaseTicket(ticket); + } + } +} diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/tree/JsTreeTable.java b/web/client-api/src/main/java/io/deephaven/web/client/api/tree/JsTreeTable.java index 18ae065378f..df2e37374fb 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/api/tree/JsTreeTable.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/tree/JsTreeTable.java @@ -41,6 +41,7 @@ import io.deephaven.web.client.api.barrage.def.InitialTableDefinition; import io.deephaven.web.client.api.barrage.stream.BiDiStream; import io.deephaven.web.client.api.filter.FilterCondition; +import io.deephaven.web.client.api.impl.TicketAndPromise; import io.deephaven.web.client.api.lifecycle.HasLifecycle; import io.deephaven.web.client.api.subscription.ViewportData; import io.deephaven.web.client.api.subscription.ViewportRow; @@ -52,7 +53,6 @@ import io.deephaven.web.shared.data.*; import io.deephaven.web.shared.data.columns.ColumnData; import jsinterop.annotations.JsIgnore; -import jsinterop.annotations.JsMethod; import jsinterop.annotations.JsNullable; import jsinterop.annotations.JsOptional; import jsinterop.annotations.JsOverlay; @@ -120,33 +120,6 @@ public class JsTreeTable extends HasLifecycle implements ServerObject { private static final double ACTION_EXPAND_WITH_DESCENDENTS = 0b011; private static final double ACTION_COLLAPSE = 0b100; - /** - * Pair of ticket and the promise that indicates it has been resolved. Tickets are usable before they are resolved, - * but to ensure that all operations completed successfully, the promise should be used to handle errors. - */ - private class TicketAndPromise { - private final Ticket ticket; - private final Promise promise; - private boolean released = false; - - private TicketAndPromise(Ticket ticket, Promise promise) { - this.ticket = ticket; - this.promise = promise; - } - - private TicketAndPromise(Ticket ticket) { - this(ticket, Promise.resolve(ticket)); - } - - public void release() { - if (!released) { - // don't double-release, in cases where the same ticket is used for multiple parts of the request - released = true; - connection.releaseTicket(ticket); - } - } - } - @TsInterface @TsName(namespace = "dh") public class TreeViewportData implements TableData { @@ -380,15 +353,15 @@ private enum RebuildStep { // The current filter and sort state private List filters = new ArrayList<>(); private List sorts = new ArrayList<>(); - private TicketAndPromise filteredTable; - private TicketAndPromise sortedTable; + private TicketAndPromise filteredTable; + private TicketAndPromise sortedTable; // Tracking for the current/next key table contents. Note that the key table doesn't necessarily // only include key columns, but all HierarchicalTable.isExpandByColumn columns. private Object[][] keyTableData; private Promise keyTable; - private TicketAndPromise viewTicket; + private TicketAndPromise viewTicket; private Promise> stream; // the "next" set of filters/sorts that we'll use. these either are "==" to the above fields, or are scheduled @@ -532,15 +505,15 @@ public JsTreeTable(WorkerConnection workerConnection, JsWidget widget) { .then(cts -> Promise.resolve(new JsTable(connection, cts)))); } - private TicketAndPromise prepareFilter() { + private TicketAndPromise prepareFilter() { if (filteredTable != null) { return filteredTable; } if (nextFilters.isEmpty()) { - return new TicketAndPromise(widget.getTicket()); + return new TicketAndPromise<>(widget.getTicket(), connection); } Ticket ticket = connection.getConfig().newTicket(); - filteredTable = new TicketAndPromise(ticket, Callbacks.grpcUnaryPromise(c -> { + filteredTable = new TicketAndPromise<>(ticket, Callbacks.grpcUnaryPromise(c -> { HierarchicalTableApplyRequest applyFilter = new HierarchicalTableApplyRequest(); applyFilter.setFiltersList( @@ -548,11 +521,11 @@ private TicketAndPromise prepareFilter() { applyFilter.setInputHierarchicalTableId(widget.getTicket()); applyFilter.setResultHierarchicalTableId(ticket); connection.hierarchicalTableServiceClient().apply(applyFilter, connection.metadata(), c::apply); - })); + }), connection); return filteredTable; } - private TicketAndPromise prepareSort(TicketAndPromise prevTicket) { + private TicketAndPromise prepareSort(TicketAndPromise prevTicket) { if (sortedTable != null) { return sortedTable; } @@ -560,14 +533,14 @@ private TicketAndPromise prepareSort(TicketAndPromise prevTicket) { return prevTicket; } Ticket ticket = connection.getConfig().newTicket(); - sortedTable = new TicketAndPromise(ticket, Callbacks.grpcUnaryPromise(c -> { + sortedTable = new TicketAndPromise<>(ticket, Callbacks.grpcUnaryPromise(c -> { HierarchicalTableApplyRequest applyFilter = new HierarchicalTableApplyRequest(); applyFilter.setSortsList(nextSort.stream().map(Sort::makeDescriptor).toArray( io.deephaven.javascript.proto.dhinternal.io.deephaven.proto.table_pb.SortDescriptor[]::new)); - applyFilter.setInputHierarchicalTableId(prevTicket.ticket); + applyFilter.setInputHierarchicalTableId(prevTicket.ticket()); applyFilter.setResultHierarchicalTableId(ticket); connection.hierarchicalTableServiceClient().apply(applyFilter, connection.metadata(), c::apply); - })); + }), connection); return sortedTable; } @@ -587,15 +560,15 @@ private Promise makeKeyTable() { return keyTable; } - private TicketAndPromise makeView(TicketAndPromise prevTicket) { + private TicketAndPromise makeView(TicketAndPromise prevTicket) { if (viewTicket != null) { return viewTicket; } Ticket ticket = connection.getConfig().newTicket(); Promise keyTable = makeKeyTable(); - viewTicket = new TicketAndPromise(ticket, Callbacks.grpcUnaryPromise(c -> { + viewTicket = new TicketAndPromise<>(ticket, Callbacks.grpcUnaryPromise(c -> { HierarchicalTableViewRequest viewRequest = new HierarchicalTableViewRequest(); - viewRequest.setHierarchicalTableId(prevTicket.ticket); + viewRequest.setHierarchicalTableId(prevTicket.ticket()); viewRequest.setResultViewId(ticket); keyTable.then(t -> { if (keyTableData[0].length > 0) { @@ -610,7 +583,7 @@ private TicketAndPromise makeView(TicketAndPromise prevTicket) { c.apply(error, null); return null; }); - })); + }), connection); return viewTicket; } @@ -652,9 +625,9 @@ private void replaceSubscription(RebuildStep step) { TicketAndPromise view = makeView(sort); return Promise.all( keyTable, - filter.promise, - sort.promise, - view.promise); + filter.promise(), + sort.promise(), + view.promise()); }) .then(results -> { BitSet columnsBitset = makeColumnSubscriptionBitset(); @@ -693,7 +666,7 @@ private void replaceSubscription(RebuildStep step) { updateInterval, 0, 0); double tableTicketOffset = BarrageSubscriptionRequest.createTicketVector(doGetRequest, - viewTicket.ticket.getTicket_asU8()); + viewTicket.ticket().getTicket_asU8()); BarrageSubscriptionRequest.startBarrageSubscriptionRequest(doGetRequest); BarrageSubscriptionRequest.addTicket(doGetRequest, tableTicketOffset); BarrageSubscriptionRequest.addColumns(doGetRequest, columnsOffset); @@ -793,7 +766,7 @@ private void handleUpdate(List nextSort, List nextFilters this.filters = nextFilters; if (fireEvent) { - CustomEventInit updatedEvent = CustomEventInit.create(); + CustomEventInit updatedEvent = CustomEventInit.create(); updatedEvent.setDetail(viewportData); fireEvent(EVENT_UPDATED, updatedEvent); } @@ -1186,7 +1159,6 @@ public Promise selectDistinct(Column[] columns) { }); } - @JsMethod public Promise getTotalsTableConfig() { // we want to communicate to the JS dev that there is no default config, so we allow // returning null here, rather than a default config. They can then easily build a @@ -1195,7 +1167,6 @@ public Promise getTotalsTableConfig() { return sourceTable.get().then(t -> Promise.resolve(t.getTotalsTableConfig())); } - @JsMethod public Promise getTotalsTable(@JsOptional Object config) { return sourceTable.get().then(t -> { // if this is the first time it is used, it might not be filtered correctly, so check that the filters match @@ -1207,7 +1178,6 @@ public Promise getTotalsTable(@JsOptional Object config) { }); } - @JsMethod public Promise getGrandTotalsTable(@JsOptional Object config) { return sourceTable.get().then(t -> Promise.resolve(t.getGrandTotalsTable(config))); }