From 7103379cbe96fa01273448c8a6d46fdb7ab1c2c1 Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Thu, 25 Jul 2024 16:24:09 -0500 Subject: [PATCH 1/7] wip --- .../web/client/ide/IdeConnection.java | 80 +++++++++++++++++++ .../deephaven/web/client/ide/IdeSession.java | 76 ++++++++++++++++++ .../web/ClientIntegrationTestSuite.java | 1 + .../web/client/api/SharedObjectTestGwt.java | 74 +++++++++++++++++ 4 files changed, 231 insertions(+) create mode 100644 web/client-api/src/test/java/io/deephaven/web/client/api/SharedObjectTestGwt.java diff --git a/web/client-api/src/main/java/io/deephaven/web/client/ide/IdeConnection.java b/web/client-api/src/main/java/io/deephaven/web/client/ide/IdeConnection.java index c5c6ceb146f..44eb2aaa159 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/ide/IdeConnection.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/ide/IdeConnection.java @@ -4,7 +4,10 @@ package io.deephaven.web.client.ide; import com.vertispan.tsdefs.annotations.TsTypeRef; +import com.vertispan.tsdefs.annotations.TsUnion; import elemental2.core.JsArray; +import elemental2.core.TypedArray; +import elemental2.core.Uint8Array; import elemental2.promise.Promise; import io.deephaven.javascript.proto.dhinternal.browserheaders.BrowserHeaders; import io.deephaven.javascript.proto.dhinternal.grpcweb.Grpc; @@ -13,8 +16,14 @@ import io.deephaven.javascript.proto.dhinternal.grpcweb.transports.transport.TransportOptions; import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.session_pb.TerminationNotificationResponse; import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.session_pb.terminationnotificationresponse.StackTrace; +import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.session_pb.ExportRequest; +import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.session_pb.PublishRequest; +import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.ticket_pb.Ticket; +import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.ticket_pb.TypedTicket; +import io.deephaven.web.client.api.Callbacks; import io.deephaven.web.client.api.ConnectOptions; import io.deephaven.web.client.api.QueryConnectable; +import io.deephaven.web.client.api.ServerObject; import io.deephaven.web.client.api.WorkerConnection; import io.deephaven.web.client.api.barrage.stream.ResponseStreamWrapper; import io.deephaven.web.client.api.console.JsVariableChanges; @@ -27,9 +36,14 @@ import io.deephaven.web.shared.fu.JsConsumer; import io.deephaven.web.shared.fu.JsRunnable; import jsinterop.annotations.JsIgnore; +import jsinterop.annotations.JsOverlay; +import jsinterop.annotations.JsPackage; import jsinterop.annotations.JsType; +import jsinterop.base.Js; import jsinterop.base.JsPropertyMap; +import java.nio.charset.StandardCharsets; + /** * Presently, this is the entrypoint into the Deephaven JS API. By creating an instance of this with the server URL and * some options, JS applications can run code on the server, and interact with available exportable objects. @@ -150,6 +164,72 @@ public JsRunnable subscribeToFieldUpdates(JsConsumer callback }; } + @TsUnion + @JsType(name = "?", namespace = JsPackage.GLOBAL, isNative = true) + public interface SharedExportBytesUnion { + @JsOverlay + static SharedExportBytesUnion of(Object o) { + return Js.cast(o); + } + @JsOverlay + default boolean isString() { + return (Object) this instanceof String; + } + + @JsOverlay + default boolean isUint8Array() { + return this instanceof Uint8Array; + } + } + + + public Promise shareObject(ServerObject object, SharedExportBytesUnion sharedTicketBytes) { + PublishRequest request = new PublishRequest(); + request.setSourceId(object.typedTicket().getTicket()); + + Ticket ticket = sharedTicketFromStringOrBytes(sharedTicketBytes); + request.setResultId(ticket); + + return Callbacks.grpcUnaryPromise(c -> { + connection.get().sessionServiceClient().publishFromTicket(request, connection.get().metadata(), c::apply); + }).then(ignore -> Promise.resolve(sharedTicketBytes)); + } + + private static Ticket sharedTicketFromStringOrBytes(SharedExportBytesUnion sharedTicketBytes) { + Ticket ticket = new Ticket(); + final int length; + final TypedArray.SetArrayUnionType array; + if (sharedTicketBytes.isString()) { + byte[] arr = sharedTicketBytes.toString().getBytes(StandardCharsets.UTF_8); + length = arr.length; + array = TypedArray.SetArrayUnionType.of(arr); + } else { + Uint8Array bytes = (Uint8Array) sharedTicketBytes; + length = bytes.length; + array = TypedArray.SetArrayUnionType.of(bytes); + } + Uint8Array bytesWithPrefix = new Uint8Array(length + 2); + bytesWithPrefix.setAt(0, (double) 'h'); + bytesWithPrefix.setAt(1, (double) '/'); + bytesWithPrefix.set(array, 2); + ticket.setTicket(bytesWithPrefix); + return ticket; + } + + public Promise getSharedObject(SharedExportBytesUnion sharedExportBytes, String type) { + TypedTicket result = new TypedTicket(); + result.setTicket(connection.get().getConfig().newTicket()); + result.setType(type); + + ExportRequest request = new ExportRequest(); + request.setSourceId(sharedTicketFromStringOrBytes(sharedExportBytes)); + request.setResultId(result.getTicket()); + + return Callbacks.grpcUnaryPromise(c -> { + connection.get().sessionServiceClient().exportFromTicket(request, connection.get().metadata(), c::apply); + }).then(ignore -> connection.get().getObject(result)); + } + @JsIgnore @Override public void notifyServerShutdown(TerminationNotificationResponse success) { diff --git a/web/client-api/src/main/java/io/deephaven/web/client/ide/IdeSession.java b/web/client-api/src/main/java/io/deephaven/web/client/ide/IdeSession.java index 2fcd025c6de..c75cd4e0c67 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/ide/IdeSession.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/ide/IdeSession.java @@ -26,6 +26,11 @@ import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.console_pb.VersionedTextDocumentIdentifier; import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.ticket_pb.Ticket; import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.console_pb.changedocumentrequest.TextDocumentContentChangeEvent; +import elemental2.core.TypedArray; +import elemental2.core.Uint8Array; +import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.session_pb.ExportRequest; +import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.session_pb.PublishRequest; +import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.ticket_pb.TypedTicket; import io.deephaven.web.client.api.*; import io.deephaven.web.client.api.barrage.stream.BiDiStream; import io.deephaven.web.client.api.console.JsCommandResult; @@ -49,6 +54,7 @@ import jsinterop.base.JsArrayLike; import jsinterop.base.JsPropertyMap; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import java.util.function.Supplier; @@ -192,6 +198,76 @@ public Promise bindTableToVariable(JsTable table, String name) { .then(ignore -> Promise.resolve((Void) null)); } + public Promise shareObject(ServerObject object, IdeConnection.SharedExportBytesUnion sharedTicketBytes) { + PublishRequest request = new PublishRequest(); + request.setSourceId(object.typedTicket().getTicket()); + + Ticket ticket = sharedTicketFromStringOrBytes(sharedTicketBytes); + request.setResultId(ticket); + + return Callbacks.grpcUnaryPromise(c -> { + connection.sessionServiceClient().publishFromTicket(request, connection.metadata(), c::apply); + }).then(ignore -> Promise.resolve(sharedTicketBytes)); + } + + private static Ticket sharedTicketFromStringOrBytes(IdeConnection.SharedExportBytesUnion sharedTicketBytes) { + Ticket ticket = new Ticket(); + final int length; + final TypedArray.SetArrayUnionType array; + if (sharedTicketBytes.isString()) { + byte[] arr = sharedTicketBytes.toString().getBytes(StandardCharsets.UTF_8); + length = arr.length; + array = TypedArray.SetArrayUnionType.of(arr); + } else { + Uint8Array bytes = (Uint8Array) sharedTicketBytes; + length = bytes.length; + array = TypedArray.SetArrayUnionType.of(bytes); + } + Uint8Array bytesWithPrefix = new Uint8Array(length + 2); + bytesWithPrefix.setAt(0, (double) 'h'); + bytesWithPrefix.setAt(1, (double) '/'); + bytesWithPrefix.set(array, 2); + ticket.setTicket(bytesWithPrefix); + return ticket; + } + + public Promise getSharedObject(IdeConnection.SharedExportBytesUnion sharedExportBytes, String type) { + if (type.equalsIgnoreCase(JsVariableType.TABLE)) { + return connection.newState((callback, newState, metadata) -> { + Ticket ticket = newState.getHandle().makeTicket(); + + ExportRequest request = new ExportRequest(); + request.setSourceId(sharedTicketFromStringOrBytes(sharedExportBytes)); + request.setResultId(ticket); + + Callbacks.grpcUnaryPromise(c -> { + connection.sessionServiceClient().exportFromTicket(request, connection.metadata(), c::apply); + }).then(ignore -> { + connection.tableServiceClient().getExportedTableCreationResponse(ticket, connection.metadata(), callback::apply); + return null; + }, err -> { + callback.apply(err, null); + return null; + }); + + }, "getSharedObject") + .refetch(this, connection.metadata()) + .then(state -> Promise.resolve(new JsTable(connection, state))); + } + + TypedTicket result = new TypedTicket(); + result.setTicket(connection.getConfig().newTicket()); + result.setType(type); + + ExportRequest request = new ExportRequest(); + request.setSourceId(sharedTicketFromStringOrBytes(sharedExportBytes)); + request.setResultId(result.getTicket()); + + return Callbacks.grpcUnaryPromise(c -> { + connection.sessionServiceClient().exportFromTicket(request, connection.metadata(), c::apply); + }).then(ignore -> connection.getObject(result)); + } + public JsRunnable subscribeToFieldUpdates(JsConsumer callback) { return connection.subscribeToFieldUpdates(callback); } diff --git a/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java b/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java index 7f3eb1c60a4..2d7e8774975 100644 --- a/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java +++ b/web/client-api/src/test/java/io/deephaven/web/ClientIntegrationTestSuite.java @@ -34,6 +34,7 @@ public static Test suite() { suite.addTestSuite(ColumnStatisticsTestGwt.class); suite.addTestSuite(GrpcTransportTestGwt.class); suite.addTestSuite(ChartDataTestGwt.class); + suite.addTestSuite(SharedObjectTestGwt.class); // This should be a unit test, but it requires a browser environment to run on GWT 2.9 // GWT 2.9 doesn't have proper bindings for Promises in HtmlUnit, so we need to use the IntegrationTest suite diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/SharedObjectTestGwt.java b/web/client-api/src/test/java/io/deephaven/web/client/api/SharedObjectTestGwt.java new file mode 100644 index 00000000000..25b099a66fc --- /dev/null +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/SharedObjectTestGwt.java @@ -0,0 +1,74 @@ +package io.deephaven.web.client.api; + +import elemental2.promise.IThenable; +import elemental2.promise.Promise; +import io.deephaven.web.client.api.console.JsVariableType; +import io.deephaven.web.client.api.subscription.ViewportData; +import io.deephaven.web.client.ide.IdeConnection; +import io.deephaven.web.client.ide.IdeSession; +import jsinterop.base.Js; + +public class SharedObjectTestGwt extends AbstractAsyncGwtTestCase { + private final TableSourceBuilder tables = new TableSourceBuilder() + .script("from deephaven import empty_table") + .script("t1 = empty_table(1).update('A=1')") + .script("t2 = empty_table(1).update('A=2')"); + private final TableSourceBuilder nothing = new TableSourceBuilder(); + public void testSharedTicket() { + connect(tables).then(client1 -> { + return connect(nothing).then(client2 -> { + delayTestFinish(220000); + return Promise.all( + // use the ascii `1` as our shared id + assertCanFetchAndShare(client1, client2, "t1", "1", 1), + // make sure we don't just test for prefixes, add a bad prefix + assertCanFetchAndShare(client1, client2, "t2", "h/1", 2), + // try an empty shared ticket, this should fail + assertCanFetchAndShare(client1, client2, "t3", "", 3) + ).then(ignore -> Promise.all( + // attempt to reuse 1 + promiseFails(fetchAndShare(client1, "t1", "1")) + )); + }); + }) + .then(this::finish).catch_(this::report); + } + + private IThenable promiseFails(Promise promise) { + return promise.then(success -> Promise.reject("expected reject"), failure -> Promise.resolve(failure)); + } + + private static Promise assertCanFetchAndShare(IdeSession client1, IdeSession client2, String tableToFetchAndShare, String ticketBytes, int cellValue) { + return fetchAndShare(client1, tableToFetchAndShare, ticketBytes) + .then(sharedTicketValue -> { + // verify the returned value is what we created + assertEquals(ticketBytes, Js.uncheckedCast(sharedTicketValue)); + + // fetch the shared object from the second client + return client2.getSharedObject(IdeConnection.SharedExportBytesUnion.of(ticketBytes), JsVariableType.TABLE); + }) + .then(value -> Promise.resolve((JsTable) value)) + .then(table -> { + // make sure we got the correct table instance + table.setViewport(0, 0); + return table.getViewportData(); + }).then(data -> { + ViewportData v = (ViewportData) data; + assertEquals(cellValue, v.getData(0, v.getColumns().getAt(0)).asInt()); + return null; + }); + } + + private static Promise fetchAndShare(IdeSession client1, String tableToFetchAndShare, String ticketBytes) { + return client1.getTable(tableToFetchAndShare, true) + .then(t1 -> { + // start with a sample table owned by the first client + return client1.shareObject(t1, IdeConnection.SharedExportBytesUnion.of(ticketBytes)); + }); + } + + @Override + public String getModuleName() { + return "io.deephaven.web.DeephavenIntegrationTest"; + } +} From 2b578a826a193d3d278e09c6dfba7281f956852b Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Thu, 25 Jul 2024 20:20:47 -0500 Subject: [PATCH 2/7] wip --- .../deephaven/web/client/api/SharedObjectTestGwt.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/SharedObjectTestGwt.java b/web/client-api/src/test/java/io/deephaven/web/client/api/SharedObjectTestGwt.java index 25b099a66fc..d5d4427a01a 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/SharedObjectTestGwt.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/SharedObjectTestGwt.java @@ -12,17 +12,20 @@ public class SharedObjectTestGwt extends AbstractAsyncGwtTestCase { private final TableSourceBuilder tables = new TableSourceBuilder() .script("from deephaven import empty_table") .script("t1 = empty_table(1).update('A=1')") - .script("t2 = empty_table(1).update('A=2')"); + .script("t2 = empty_table(1).update('A=2')") + .script("t3 = empty_table(1).update('A=3')"); private final TableSourceBuilder nothing = new TableSourceBuilder(); public void testSharedTicket() { connect(tables).then(client1 -> { return connect(nothing).then(client2 -> { delayTestFinish(220000); - return Promise.all( + return // use the ascii `1` as our shared id - assertCanFetchAndShare(client1, client2, "t1", "1", 1), + assertCanFetchAndShare(client1, client2, "t1", "1", 1) + .then(ignore1 -> // make sure we don't just test for prefixes, add a bad prefix - assertCanFetchAndShare(client1, client2, "t2", "h/1", 2), + assertCanFetchAndShare(client1, client2, "t2", "h/1", 2)) + .then(ignore -> // try an empty shared ticket, this should fail assertCanFetchAndShare(client1, client2, "t3", "", 3) ).then(ignore -> Promise.all( From 0bf1ac53ed3ad91365e08a43de93b3252466545a Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Fri, 26 Jul 2024 15:08:26 -0500 Subject: [PATCH 3/7] Finish tests, safeguards, javadoc --- .../web/client/api/JsPartitionedTable.java | 6 + .../web/client/api/JsTotalsTable.java | 5 + .../web/client/api/ServerObject.java | 9 ++ .../web/client/api/WorkerConnection.java | 78 +++++++++++- .../web/client/api/tree/JsTreeTable.java | 6 + .../web/client/api/widget/JsWidget.java | 5 + .../api/widget/JsWidgetExportedObject.java | 5 + .../web/client/ide/IdeConnection.java | 110 ++++++---------- .../deephaven/web/client/ide/IdeSession.java | 117 +++++++----------- .../client/ide/SharedExportBytesUnion.java | 30 +++++ .../web/client/api/SharedObjectTestGwt.java | 98 +++++++++------ 11 files changed, 279 insertions(+), 190 deletions(-) create mode 100644 web/client-api/src/main/java/io/deephaven/web/client/ide/SharedExportBytesUnion.java diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/JsPartitionedTable.java b/web/client-api/src/main/java/io/deephaven/web/client/api/JsPartitionedTable.java index f925abd68c8..fc6729f451d 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/api/JsPartitionedTable.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/JsPartitionedTable.java @@ -70,6 +70,12 @@ public JsPartitionedTable(WorkerConnection connection, JsWidget widget) { this.widget = widget; } + @JsIgnore + @Override + public WorkerConnection getConnection() { + return connection; + } + @JsIgnore public Promise refetch() { closeSubscriptions(); diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/JsTotalsTable.java b/web/client-api/src/main/java/io/deephaven/web/client/api/JsTotalsTable.java index 02dc7ba54e7..3b904d569c5 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/api/JsTotalsTable.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/JsTotalsTable.java @@ -67,6 +67,11 @@ public JsTotalsTable(JsTable wrappedTable, String directive, JsArray gro this.groupBy = Js.uncheckedCast(groupBy.slice()); } + @Override + public WorkerConnection getConnection() { + return wrappedTable.getConnection(); + } + public void refreshViewport() { if (firstRow != null && lastRow != null) { setViewport(firstRow, lastRow, Js.uncheckedCast(columns), updateIntervalMs, null); diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/ServerObject.java b/web/client-api/src/main/java/io/deephaven/web/client/api/ServerObject.java index 11fa9f863ae..c11a1acfc74 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/api/ServerObject.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/ServerObject.java @@ -13,6 +13,7 @@ import jsinterop.annotations.JsOverlay; import jsinterop.annotations.JsPackage; import jsinterop.annotations.JsType; +import jsinterop.base.Js; /** * Indicates that this object is a local representation of an object that exists on the server. Similar to HasLifecycle, @@ -24,6 +25,9 @@ public interface ServerObject { @JsIgnore TypedTicket typedTicket(); + @JsIgnore + WorkerConnection getConnection(); + /** * Note that we don't explicitly use this as a union type, but sort of as a way to pretend that ServerObject is a * sealed type with this generated set of subtypes. @@ -31,6 +35,11 @@ public interface ServerObject { @JsType(name = "?", namespace = JsPackage.GLOBAL, isNative = true) @TsUnion interface Union { + @JsOverlay + static Union of(Object object) { + return Js.uncheckedCast(object); + } + @TsUnionMember @JsOverlay default JsTable asTable() { 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 8d384665c92..07f4a33dd81 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 @@ -9,6 +9,7 @@ import elemental2.core.JsObject; import elemental2.core.JsSet; import elemental2.core.JsWeakMap; +import elemental2.core.TypedArray; import elemental2.core.Uint8Array; import elemental2.dom.DomGlobal; import elemental2.promise.Promise; @@ -38,6 +39,7 @@ import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.partitionedtable_pb_service.PartitionedTableServiceClient; import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.session_pb.ExportRequest; import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.session_pb.ExportResponse; +import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.session_pb.PublishRequest; import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.session_pb.ReleaseRequest; import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.session_pb.TerminationNotificationRequest; import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.session_pb_service.SessionServiceClient; @@ -78,6 +80,7 @@ import io.deephaven.web.client.fu.JsItr; import io.deephaven.web.client.fu.JsLog; import io.deephaven.web.client.fu.LazyPromise; +import io.deephaven.web.client.ide.SharedExportBytesUnion; import io.deephaven.web.client.state.ClientTableState; import io.deephaven.web.client.state.HasTableBinding; import io.deephaven.web.client.state.TableReviver; @@ -98,6 +101,7 @@ import org.apache.arrow.flatbuf.Schema; import javax.annotation.Nullable; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -774,7 +778,79 @@ private static void warnLegacyTicketTypes(String ticketType) { } } - @JsMethod + public Promise shareObject(ServerObject object, SharedExportBytesUnion sharedTicketBytes) { + if (object.getConnection() != this) { + return Promise.reject("Cannot share an object that comes from another server instance"); + } + PublishRequest request = new PublishRequest(); + request.setSourceId(object.typedTicket().getTicket()); + + Ticket ticket = sharedTicketFromStringOrBytes(sharedTicketBytes); + request.setResultId(ticket); + + return Callbacks.grpcUnaryPromise(c -> { + sessionServiceClient().publishFromTicket(request, metadata(), c::apply); + }).then(ignore -> Promise.resolve(sharedTicketBytes)); + } + + private static Ticket sharedTicketFromStringOrBytes(SharedExportBytesUnion sharedTicketBytes) { + Ticket ticket = new Ticket(); + final int length; + final TypedArray.SetArrayUnionType array; + if (sharedTicketBytes.isString()) { + byte[] arr = sharedTicketBytes.toString().getBytes(StandardCharsets.UTF_8); + length = arr.length; + array = TypedArray.SetArrayUnionType.of(arr); + } else { + Uint8Array bytes = (Uint8Array) sharedTicketBytes; + length = bytes.length; + array = TypedArray.SetArrayUnionType.of(bytes); + } + Uint8Array bytesWithPrefix = new Uint8Array(length + 2); + bytesWithPrefix.setAt(0, (double) 'h'); + bytesWithPrefix.setAt(1, (double) '/'); + bytesWithPrefix.set(array, 2); + ticket.setTicket(bytesWithPrefix); + return ticket; + } + + public Promise getSharedObject(SharedExportBytesUnion sharedExportBytes, String type) { + if (type.equalsIgnoreCase(JsVariableType.TABLE)) { + return newState((callback, newState, metadata) -> { + Ticket ticket = newState.getHandle().makeTicket(); + + ExportRequest request = new ExportRequest(); + request.setSourceId(sharedTicketFromStringOrBytes(sharedExportBytes)); + request.setResultId(ticket); + + Callbacks.grpcUnaryPromise(c -> { + sessionServiceClient().exportFromTicket(request, metadata(), c::apply); + }).then(ignore -> { + tableServiceClient().getExportedTableCreationResponse(ticket, metadata(), callback::apply); + return null; + }, err -> { + callback.apply(err, null); + return null; + }); + + }, "getSharedObject") + .refetch(null, metadata()) + .then(state -> Promise.resolve(new JsTable(this, state))); + } + + TypedTicket result = new TypedTicket(); + result.setTicket(getConfig().newTicket()); + result.setType(type); + + ExportRequest request = new ExportRequest(); + request.setSourceId(sharedTicketFromStringOrBytes(sharedExportBytes)); + request.setResultId(result.getTicket()); + + return Callbacks.grpcUnaryPromise(c -> { + sessionServiceClient().exportFromTicket(request, metadata(), c::apply); + }).then(ignore -> getObject(result)); + } + @SuppressWarnings("ConstantConditions") public JsRunnable subscribeToFieldUpdates(JsConsumer callback) { fieldUpdatesCallback.add(callback); 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 ba5686df6d1..5ced188ea4e 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 @@ -293,6 +293,12 @@ public JsTreeTable(WorkerConnection workerConnection, JsWidget widget) { .then(cts -> Promise.resolve(new JsTable(connection, cts)))); } + @JsIgnore + @Override + public WorkerConnection getConnection() { + return connection; + } + private TicketAndPromise prepareFilter() { if (filteredTable != null) { return filteredTable; diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/widget/JsWidget.java b/web/client-api/src/main/java/io/deephaven/web/client/api/widget/JsWidget.java index aa99597ff0f..c25a020624b 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/api/widget/JsWidget.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/widget/JsWidget.java @@ -121,6 +121,11 @@ public JsWidget(WorkerConnection connection, TypedTicket typedTicket) { this.exportedObjects = new JsArray<>(); } + @Override + public WorkerConnection getConnection() { + return connection; + } + private void closeStream() { if (messageStream != null) { messageStream.end(); diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/widget/JsWidgetExportedObject.java b/web/client-api/src/main/java/io/deephaven/web/client/api/widget/JsWidgetExportedObject.java index f7169013557..5c7fdf4c7ba 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/api/widget/JsWidgetExportedObject.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/widget/JsWidgetExportedObject.java @@ -62,6 +62,11 @@ public JsWidgetExportedObject(WorkerConnection connection, TypedTicket ticket) { }); } + @Override + public WorkerConnection getConnection() { + return connection; + } + /** * Returns the type of this export, typically one of {@link JsVariableType}, but may also include plugin types. If * null, this object cannot be fetched, but can be passed to the server, such as via diff --git a/web/client-api/src/main/java/io/deephaven/web/client/ide/IdeConnection.java b/web/client-api/src/main/java/io/deephaven/web/client/ide/IdeConnection.java index 44eb2aaa159..a5accd80705 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/ide/IdeConnection.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/ide/IdeConnection.java @@ -4,10 +4,7 @@ package io.deephaven.web.client.ide; import com.vertispan.tsdefs.annotations.TsTypeRef; -import com.vertispan.tsdefs.annotations.TsUnion; import elemental2.core.JsArray; -import elemental2.core.TypedArray; -import elemental2.core.Uint8Array; import elemental2.promise.Promise; import io.deephaven.javascript.proto.dhinternal.browserheaders.BrowserHeaders; import io.deephaven.javascript.proto.dhinternal.grpcweb.Grpc; @@ -16,11 +13,6 @@ import io.deephaven.javascript.proto.dhinternal.grpcweb.transports.transport.TransportOptions; import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.session_pb.TerminationNotificationResponse; import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.session_pb.terminationnotificationresponse.StackTrace; -import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.session_pb.ExportRequest; -import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.session_pb.PublishRequest; -import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.ticket_pb.Ticket; -import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.ticket_pb.TypedTicket; -import io.deephaven.web.client.api.Callbacks; import io.deephaven.web.client.api.ConnectOptions; import io.deephaven.web.client.api.QueryConnectable; import io.deephaven.web.client.api.ServerObject; @@ -36,14 +28,9 @@ import io.deephaven.web.shared.fu.JsConsumer; import io.deephaven.web.shared.fu.JsRunnable; import jsinterop.annotations.JsIgnore; -import jsinterop.annotations.JsOverlay; -import jsinterop.annotations.JsPackage; import jsinterop.annotations.JsType; -import jsinterop.base.Js; import jsinterop.base.JsPropertyMap; -import java.nio.charset.StandardCharsets; - /** * Presently, this is the entrypoint into the Deephaven JS API. By creating an instance of this with the server URL and * some options, JS applications can run code on the server, and interact with available exportable objects. @@ -164,70 +151,43 @@ public JsRunnable subscribeToFieldUpdates(JsConsumer callback }; } - @TsUnion - @JsType(name = "?", namespace = JsPackage.GLOBAL, isNative = true) - public interface SharedExportBytesUnion { - @JsOverlay - static SharedExportBytesUnion of(Object o) { - return Js.cast(o); - } - @JsOverlay - default boolean isString() { - return (Object) this instanceof String; - } - - @JsOverlay - default boolean isUint8Array() { - return this instanceof Uint8Array; - } - } - - - public Promise shareObject(ServerObject object, SharedExportBytesUnion sharedTicketBytes) { - PublishRequest request = new PublishRequest(); - request.setSourceId(object.typedTicket().getTicket()); - - Ticket ticket = sharedTicketFromStringOrBytes(sharedTicketBytes); - request.setResultId(ticket); - - return Callbacks.grpcUnaryPromise(c -> { - connection.get().sessionServiceClient().publishFromTicket(request, connection.get().metadata(), c::apply); - }).then(ignore -> Promise.resolve(sharedTicketBytes)); - } - - private static Ticket sharedTicketFromStringOrBytes(SharedExportBytesUnion sharedTicketBytes) { - Ticket ticket = new Ticket(); - final int length; - final TypedArray.SetArrayUnionType array; - if (sharedTicketBytes.isString()) { - byte[] arr = sharedTicketBytes.toString().getBytes(StandardCharsets.UTF_8); - length = arr.length; - array = TypedArray.SetArrayUnionType.of(arr); - } else { - Uint8Array bytes = (Uint8Array) sharedTicketBytes; - length = bytes.length; - array = TypedArray.SetArrayUnionType.of(bytes); - } - Uint8Array bytesWithPrefix = new Uint8Array(length + 2); - bytesWithPrefix.setAt(0, (double) 'h'); - bytesWithPrefix.setAt(1, (double) '/'); - bytesWithPrefix.set(array, 2); - ticket.setTicket(bytesWithPrefix); - return ticket; + /** + * Makes an {@code object} available to another user or another client on this same server which knows the value of + * the {@code sharedTicketBytes}. Use that sharedTicketBytes value like a one-time use password - any other client + * which knows this value can read the same object. + *

+ * Shared objects will remain available using the sharedTicketBytes until the client that first shared them + * releases/closes their copy of the object. Whatever side-channel is used to share the bytes, be sure to wait until + * the remote end has signaled that it has successfully fetched the object before releasing it from this client. + *

+ * Be sure to use an unpredictable value for the shared ticket bytes, like a UUID or other large, random value to + * prevent accidental access by other clients. + * + * @param object the object to share with another client/user + * @param sharedTicketBytes the value which another client/user must know to obtain the object. It may be a unicode + * string (will be encoded as utf8 bytes), or a {@link elemental2.core.Uint8Array} value. + * @return A promise that will resolve to the value passed as sharedTicketBytes when the object is ready to be read + * by another client, or will reject if an error occurs. + */ + public Promise shareObject(ServerObject.Union object, + SharedExportBytesUnion sharedTicketBytes) { + return connection.get().shareObject(object.asServerObject(), sharedTicketBytes); } - public Promise getSharedObject(SharedExportBytesUnion sharedExportBytes, String type) { - TypedTicket result = new TypedTicket(); - result.setTicket(connection.get().getConfig().newTicket()); - result.setType(type); - - ExportRequest request = new ExportRequest(); - request.setSourceId(sharedTicketFromStringOrBytes(sharedExportBytes)); - request.setResultId(result.getTicket()); - - return Callbacks.grpcUnaryPromise(c -> { - connection.get().sessionServiceClient().exportFromTicket(request, connection.get().metadata(), c::apply); - }).then(ignore -> connection.get().getObject(result)); + /** + * Reads an object shared by another client to this server with the {@code sharedTicketBytes}. Until the other + * client releases this object (or their session ends), the object will be available on the server. + *

+ * The type of the object must be passed so that the object can be read from the server correct - the other client + * should provide this information. + * + * @param sharedTicketBytes the value provided by another client/user to obtain the object. It may be a unicode + * string (will be encoded as utf8 bytes), or a {@link elemental2.core.Uint8Array} value. + * @param type The type of the object, so it can be correctly read from the server + * @return A promise that will resolve to the shared object, or will reject with an error if it cannot be read. + */ + public Promise getSharedObject(SharedExportBytesUnion sharedTicketBytes, String type) { + return connection.get().getSharedObject(sharedTicketBytes, type); } @JsIgnore diff --git a/web/client-api/src/main/java/io/deephaven/web/client/ide/IdeSession.java b/web/client-api/src/main/java/io/deephaven/web/client/ide/IdeSession.java index c75cd4e0c67..41b9c8b5250 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/ide/IdeSession.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/ide/IdeSession.java @@ -24,14 +24,15 @@ import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.console_pb.Position; import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.console_pb.TextDocumentItem; import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.console_pb.VersionedTextDocumentIdentifier; -import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.ticket_pb.Ticket; import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.console_pb.changedocumentrequest.TextDocumentContentChangeEvent; -import elemental2.core.TypedArray; -import elemental2.core.Uint8Array; -import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.session_pb.ExportRequest; -import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.session_pb.PublishRequest; -import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.ticket_pb.TypedTicket; -import io.deephaven.web.client.api.*; +import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.ticket_pb.Ticket; +import io.deephaven.web.client.api.Callbacks; +import io.deephaven.web.client.api.DateWrapper; +import io.deephaven.web.client.api.JsPartitionedTable; +import io.deephaven.web.client.api.JsTable; +import io.deephaven.web.client.api.LogItem; +import io.deephaven.web.client.api.ServerObject; +import io.deephaven.web.client.api.WorkerConnection; import io.deephaven.web.client.api.barrage.stream.BiDiStream; import io.deephaven.web.client.api.console.JsCommandResult; import io.deephaven.web.client.api.console.JsVariableChanges; @@ -54,7 +55,6 @@ import jsinterop.base.JsArrayLike; import jsinterop.base.JsPropertyMap; -import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import java.util.function.Supplier; @@ -198,74 +198,43 @@ public Promise bindTableToVariable(JsTable table, String name) { .then(ignore -> Promise.resolve((Void) null)); } - public Promise shareObject(ServerObject object, IdeConnection.SharedExportBytesUnion sharedTicketBytes) { - PublishRequest request = new PublishRequest(); - request.setSourceId(object.typedTicket().getTicket()); - - Ticket ticket = sharedTicketFromStringOrBytes(sharedTicketBytes); - request.setResultId(ticket); - - return Callbacks.grpcUnaryPromise(c -> { - connection.sessionServiceClient().publishFromTicket(request, connection.metadata(), c::apply); - }).then(ignore -> Promise.resolve(sharedTicketBytes)); - } - - private static Ticket sharedTicketFromStringOrBytes(IdeConnection.SharedExportBytesUnion sharedTicketBytes) { - Ticket ticket = new Ticket(); - final int length; - final TypedArray.SetArrayUnionType array; - if (sharedTicketBytes.isString()) { - byte[] arr = sharedTicketBytes.toString().getBytes(StandardCharsets.UTF_8); - length = arr.length; - array = TypedArray.SetArrayUnionType.of(arr); - } else { - Uint8Array bytes = (Uint8Array) sharedTicketBytes; - length = bytes.length; - array = TypedArray.SetArrayUnionType.of(bytes); - } - Uint8Array bytesWithPrefix = new Uint8Array(length + 2); - bytesWithPrefix.setAt(0, (double) 'h'); - bytesWithPrefix.setAt(1, (double) '/'); - bytesWithPrefix.set(array, 2); - ticket.setTicket(bytesWithPrefix); - return ticket; + /** + * Makes the {@code object} available to another user or another client on this same server which knows the value of + * the {@code sharedTicketBytes}. Use that sharedTicketBytes value like a one-time use password - any other client + * which knows this value can read the same object. + *

+ * Shared objects will remain available using the sharedTicketBytes until the client that first shared them + * releases/closes their copy of the object. Whatever side-channel is used to share the bytes, be sure to wait until + * the remote end has signaled that it has successfully fetched the object before releasing it from this client. + *

+ * Be sure to use an unpredictable value for the shared ticket bytes, like a UUID or other large, random value to + * prevent accidental access by other clients. + * + * @param object the object to share with another client/user + * @param sharedTicketBytes the value which another client/user must know to obtain the object. It may be a unicode + * string (will be encoded as utf8 bytes), or a {@link elemental2.core.Uint8Array} value. + * @return A promise that will resolve to the value passed as sharedTicketBytes when the object is ready to be read + * by another client, or will reject if an error occurs. + */ + public Promise shareObject(ServerObject.Union object, + SharedExportBytesUnion sharedTicketBytes) { + return connection.shareObject(object.asServerObject(), sharedTicketBytes); } - public Promise getSharedObject(IdeConnection.SharedExportBytesUnion sharedExportBytes, String type) { - if (type.equalsIgnoreCase(JsVariableType.TABLE)) { - return connection.newState((callback, newState, metadata) -> { - Ticket ticket = newState.getHandle().makeTicket(); - - ExportRequest request = new ExportRequest(); - request.setSourceId(sharedTicketFromStringOrBytes(sharedExportBytes)); - request.setResultId(ticket); - - Callbacks.grpcUnaryPromise(c -> { - connection.sessionServiceClient().exportFromTicket(request, connection.metadata(), c::apply); - }).then(ignore -> { - connection.tableServiceClient().getExportedTableCreationResponse(ticket, connection.metadata(), callback::apply); - return null; - }, err -> { - callback.apply(err, null); - return null; - }); - - }, "getSharedObject") - .refetch(this, connection.metadata()) - .then(state -> Promise.resolve(new JsTable(connection, state))); - } - - TypedTicket result = new TypedTicket(); - result.setTicket(connection.getConfig().newTicket()); - result.setType(type); - - ExportRequest request = new ExportRequest(); - request.setSourceId(sharedTicketFromStringOrBytes(sharedExportBytes)); - request.setResultId(result.getTicket()); - - return Callbacks.grpcUnaryPromise(c -> { - connection.sessionServiceClient().exportFromTicket(request, connection.metadata(), c::apply); - }).then(ignore -> connection.getObject(result)); + /** + * Reads an object shared by another client to this server with the {@code sharedTicketBytes}. Until the other + * client releases this object (or their session ends), the object will be available on the server. + *

+ * The type of the object must be passed so that the object can be read from the server correct - the other client + * should provide this information. + * + * @param sharedTicketBytes the value provided by another client/user to obtain the object. It may be a unicode + * string (will be encoded as utf8 bytes), or a {@link elemental2.core.Uint8Array} value. + * @param type The type of the object, so it can be correctly read from the server + * @return A promise that will resolve to the shared object, or will reject with an error if it cannot be read. + */ + public Promise getSharedObject(SharedExportBytesUnion sharedTicketBytes, String type) { + return connection.getSharedObject(sharedTicketBytes, type); } public JsRunnable subscribeToFieldUpdates(JsConsumer callback) { diff --git a/web/client-api/src/main/java/io/deephaven/web/client/ide/SharedExportBytesUnion.java b/web/client-api/src/main/java/io/deephaven/web/client/ide/SharedExportBytesUnion.java new file mode 100644 index 00000000000..de0c1c79965 --- /dev/null +++ b/web/client-api/src/main/java/io/deephaven/web/client/ide/SharedExportBytesUnion.java @@ -0,0 +1,30 @@ +// +// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending +// +package io.deephaven.web.client.ide; + +import com.vertispan.tsdefs.annotations.TsUnion; +import elemental2.core.Uint8Array; +import jsinterop.annotations.JsOverlay; +import jsinterop.annotations.JsPackage; +import jsinterop.annotations.JsType; +import jsinterop.base.Js; + +@TsUnion +@JsType(name = "?", namespace = JsPackage.GLOBAL, isNative = true) +public interface SharedExportBytesUnion { + @JsOverlay + static SharedExportBytesUnion of(Object o) { + return Js.cast(o); + } + + @JsOverlay + default boolean isString() { + return (Object) this instanceof String; + } + + @JsOverlay + default boolean isUint8Array() { + return this instanceof Uint8Array; + } +} diff --git a/web/client-api/src/test/java/io/deephaven/web/client/api/SharedObjectTestGwt.java b/web/client-api/src/test/java/io/deephaven/web/client/api/SharedObjectTestGwt.java index d5d4427a01a..7dd376ac42a 100644 --- a/web/client-api/src/test/java/io/deephaven/web/client/api/SharedObjectTestGwt.java +++ b/web/client-api/src/test/java/io/deephaven/web/client/api/SharedObjectTestGwt.java @@ -1,11 +1,13 @@ +// +// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending +// package io.deephaven.web.client.api; -import elemental2.promise.IThenable; import elemental2.promise.Promise; import io.deephaven.web.client.api.console.JsVariableType; import io.deephaven.web.client.api.subscription.ViewportData; -import io.deephaven.web.client.ide.IdeConnection; import io.deephaven.web.client.ide.IdeSession; +import io.deephaven.web.client.ide.SharedExportBytesUnion; import jsinterop.base.Js; public class SharedObjectTestGwt extends AbstractAsyncGwtTestCase { @@ -15,58 +17,74 @@ public class SharedObjectTestGwt extends AbstractAsyncGwtTestCase { .script("t2 = empty_table(1).update('A=2')") .script("t3 = empty_table(1).update('A=3')"); private final TableSourceBuilder nothing = new TableSourceBuilder(); + public void testSharedTicket() { - connect(tables).then(client1 -> { - return connect(nothing).then(client2 -> { - delayTestFinish(220000); - return - // use the ascii `1` as our shared id - assertCanFetchAndShare(client1, client2, "t1", "1", 1) - .then(ignore1 -> - // make sure we don't just test for prefixes, add a bad prefix - assertCanFetchAndShare(client1, client2, "t2", "h/1", 2)) - .then(ignore -> - // try an empty shared ticket, this should fail - assertCanFetchAndShare(client1, client2, "t3", "", 3) - ).then(ignore -> Promise.all( - // attempt to reuse 1 - promiseFails(fetchAndShare(client1, "t1", "1")) - )); - }); - }) - .then(this::finish).catch_(this::report); + connect(tables).then(client1 -> connect(nothing).then(client2 -> { + delayTestFinish(220000); + return Promise.all( + // use the ascii `1` as our shared id + assertCanFetchAndShare(client1, client2, "t1", "1", 1) + // try to re-fetch the shared ticket "1" + .then(ignore -> getShared(client2, "1", 1)), + // make sure we don't just test for prefixes, add a bad prefix + assertCanFetchAndShare(client1, client2, "t2", "h/1", 2), + // try an empty shared ticket, this should fail + assertCanFetchAndShare(client1, client2, "t3", "", 3)) + + // Try some tests that will fail, now that the above shares have worked + .then(ignore -> Promise.all( + // attempt to reuse "1" as a publish target + promiseFails(fetchAndShare(client1, "t1", "1")), + // attempt to fetch a non-existent ticket + promiseFails( + client2.getSharedObject(SharedExportBytesUnion.of("987"), JsVariableType.TABLE)))); + })) + .then(this::finish).catch_(this::report); } - private IThenable promiseFails(Promise promise) { - return promise.then(success -> Promise.reject("expected reject"), failure -> Promise.resolve(failure)); + private Promise promiseFails(Promise promise) { + return promise.then(success -> Promise.reject("expected reject"), Promise::resolve); } - private static Promise assertCanFetchAndShare(IdeSession client1, IdeSession client2, String tableToFetchAndShare, String ticketBytes, int cellValue) { - return fetchAndShare(client1, tableToFetchAndShare, ticketBytes) + private static Promise assertCanFetchAndShare(IdeSession client1, IdeSession client2, + String tableToFetchAndShare, String sharedTicketBytes, int cellValue) { + return fetchAndShare(client1, tableToFetchAndShare, sharedTicketBytes) .then(sharedTicketValue -> { // verify the returned value is what we created - assertEquals(ticketBytes, Js.uncheckedCast(sharedTicketValue)); + assertEquals(sharedTicketBytes, Js.uncheckedCast(sharedTicketValue)); - // fetch the shared object from the second client - return client2.getSharedObject(IdeConnection.SharedExportBytesUnion.of(ticketBytes), JsVariableType.TABLE); - }) - .then(value -> Promise.resolve((JsTable) value)) - .then(table -> { - // make sure we got the correct table instance - table.setViewport(0, 0); - return table.getViewportData(); - }).then(data -> { - ViewportData v = (ViewportData) data; - assertEquals(cellValue, v.getData(0, v.getColumns().getAt(0)).asInt()); - return null; + // ask the other client to get that shared object, confirm we can read it + return getShared(client2, sharedTicketBytes, cellValue); }); } - private static Promise fetchAndShare(IdeSession client1, String tableToFetchAndShare, String ticketBytes) { + /** + * Helper that just fetches a scope ticket and shares it. + */ + private static Promise fetchAndShare(IdeSession client1, String tableToFetchAndShare, + String sharedTicketBytes) { return client1.getTable(tableToFetchAndShare, true) .then(t1 -> { // start with a sample table owned by the first client - return client1.shareObject(t1, IdeConnection.SharedExportBytesUnion.of(ticketBytes)); + return client1.shareObject(ServerObject.Union.of(t1), SharedExportBytesUnion.of(sharedTicketBytes)); + }); + } + + /** + * Helper that just reads a shared ticket. + */ + private static Promise getShared(IdeSession client2, String sharedTicketBytes, int cellValue) { + // fetch the shared object from the second client + return client2.getSharedObject(SharedExportBytesUnion.of(sharedTicketBytes), JsVariableType.TABLE) + .then(value -> Promise.resolve((JsTable) value)) + .then(table -> { + // make sure we got the correct table instance + table.setViewport(0, 0); + return table.getViewportData().then(data -> { + ViewportData v = (ViewportData) data; + assertEquals(cellValue, v.getData(0, v.getColumns().getAt(0)).asInt()); + return Promise.resolve(table); + }); }); } From 3446a3722d44de85dae24bb5da6b6cd07f500733 Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Wed, 31 Jul 2024 14:20:24 -0500 Subject: [PATCH 4/7] javadoc suggestions --- .../main/java/io/deephaven/web/client/ide/IdeConnection.java | 2 +- .../src/main/java/io/deephaven/web/client/ide/IdeSession.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/client-api/src/main/java/io/deephaven/web/client/ide/IdeConnection.java b/web/client-api/src/main/java/io/deephaven/web/client/ide/IdeConnection.java index a5accd80705..2107f54492d 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/ide/IdeConnection.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/ide/IdeConnection.java @@ -161,7 +161,7 @@ public JsRunnable subscribeToFieldUpdates(JsConsumer callback * the remote end has signaled that it has successfully fetched the object before releasing it from this client. *

* Be sure to use an unpredictable value for the shared ticket bytes, like a UUID or other large, random value to - * prevent accidental access by other clients. + * prevent access by unauthorized clients. * * @param object the object to share with another client/user * @param sharedTicketBytes the value which another client/user must know to obtain the object. It may be a unicode diff --git a/web/client-api/src/main/java/io/deephaven/web/client/ide/IdeSession.java b/web/client-api/src/main/java/io/deephaven/web/client/ide/IdeSession.java index 41b9c8b5250..c46f4c8f3c6 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/ide/IdeSession.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/ide/IdeSession.java @@ -208,7 +208,7 @@ public Promise bindTableToVariable(JsTable table, String name) { * the remote end has signaled that it has successfully fetched the object before releasing it from this client. *

* Be sure to use an unpredictable value for the shared ticket bytes, like a UUID or other large, random value to - * prevent accidental access by other clients. + * prevent access by unauthorized clients. * * @param object the object to share with another client/user * @param sharedTicketBytes the value which another client/user must know to obtain the object. It may be a unicode From 574a2db36b98ac90a2b137481bad76aaa7593eec Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Thu, 15 Aug 2024 13:27:35 -0500 Subject: [PATCH 5/7] Add a comment --- .../main/java/io/deephaven/web/client/api/WorkerConnection.java | 1 + 1 file changed, 1 insertion(+) 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 07f4a33dd81..3c703feba90 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 @@ -807,6 +807,7 @@ private static Ticket sharedTicketFromStringOrBytes(SharedExportBytesUnion share array = TypedArray.SetArrayUnionType.of(bytes); } Uint8Array bytesWithPrefix = new Uint8Array(length + 2); + // Add the shared ticket prefix at the start of the provided value bytesWithPrefix.setAt(0, (double) 'h'); bytesWithPrefix.setAt(1, (double) '/'); bytesWithPrefix.set(array, 2); From 7e88120847ed17c633b80c1cb8617c82a8cfbeeb Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Thu, 16 Jan 2025 21:45:07 -0600 Subject: [PATCH 6/7] Move ticket creation wiring in the JS api to a single class --- .../web/client/api/ClientConfiguration.java | 51 ------ .../io/deephaven/web/client/api/JsTable.java | 6 +- .../web/client/api/QueryConnectable.java | 5 +- .../deephaven/web/client/api/TableTicket.java | 16 +- .../io/deephaven/web/client/api/Tickets.java | 160 ++++++++++++++++++ .../web/client/api/WorkerConnection.java | 45 ++--- .../api/console/JsVariableDefinition.java | 7 +- .../web/client/api/input/JsInputTable.java | 4 +- .../web/client/api/tree/JsTreeTable.java | 6 +- .../api/widget/JsWidgetExportedObject.java | 2 +- 10 files changed, 191 insertions(+), 111 deletions(-) delete mode 100644 web/client-api/src/main/java/io/deephaven/web/client/api/ClientConfiguration.java create mode 100644 web/client-api/src/main/java/io/deephaven/web/client/api/Tickets.java diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/ClientConfiguration.java b/web/client-api/src/main/java/io/deephaven/web/client/api/ClientConfiguration.java deleted file mode 100644 index d75f21b4802..00000000000 --- a/web/client-api/src/main/java/io/deephaven/web/client/api/ClientConfiguration.java +++ /dev/null @@ -1,51 +0,0 @@ -// -// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending -// -package io.deephaven.web.client.api; - -import elemental2.core.Uint8Array; -import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.ticket_pb.Ticket; - -/** - * A place to assemble various "services" we want to make ubiquitously available in the client by passing around a - * single object. - */ -public class ClientConfiguration { - private static final byte EXPORT_PREFIX = 'e'; - - /** - * The next number to use when making a ticket. These values must always be positive, as zero is an invalid value, - * and negative values represent server-created tickets. - */ - private int next = 1; - - public ClientConfiguration() {} - - public Ticket newTicket() { - Ticket ticket = new Ticket(); - ticket.setTicket(newTicketRaw()); - return ticket; - } - - public int newTicketInt() { - return next++; - } - - public Uint8Array newTicketRaw() { - if (next == Integer.MAX_VALUE) { - throw new IllegalStateException("Ran out of tickets!"); - } - - final int exportId = next++; - final double[] dest = new double[5]; - dest[0] = EXPORT_PREFIX; - dest[1] = (byte) exportId; - dest[2] = (byte) (exportId >>> 8); - dest[3] = (byte) (exportId >>> 16); - dest[4] = (byte) (exportId >>> 24); - - final Uint8Array bytes = new Uint8Array(5); - bytes.set(dest); - return bytes; - } -} 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 a268962d2a6..afab9abc432 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 @@ -1044,7 +1044,7 @@ public Promise rollup(@TsTypeRef(JsRollupConfig.class) Object confi config = new JsRollupConfig(Js.cast(configObject)); } - Ticket rollupTicket = workerConnection.getConfig().newTicket(); + Ticket rollupTicket = workerConnection.getTickets().newExportTicket(); Promise rollupPromise = Callbacks.grpcUnaryPromise(c -> { RollupRequest request = config.buildRequest(getColumns()); @@ -1080,7 +1080,7 @@ public Promise treeTable(@TsTypeRef(JsTreeTableConfig.class) Object config = new JsTreeTableConfig(Js.cast(configObject)); } - Ticket treeTicket = workerConnection.getConfig().newTicket(); + Ticket treeTicket = workerConnection.getTickets().newExportTicket(); Promise treePromise = Callbacks.grpcUnaryPromise(c -> { TreeRequest requestMessage = new TreeRequest(); @@ -1297,7 +1297,7 @@ public Promise partitionBy(Object keys, @JsOptional Boolean // Start the partitionBy on the server - we want to get the error from here, but we'll race the fetch against // this to avoid an extra round-trip - Ticket partitionedTableTicket = workerConnection.getConfig().newTicket(); + Ticket partitionedTableTicket = workerConnection.getTickets().newExportTicket(); Promise partitionByPromise = Callbacks.grpcUnaryPromise(c -> { PartitionByRequest partitionBy = new PartitionByRequest(); partitionBy.setTableId(state().getHandle().makeTicket()); diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/QueryConnectable.java b/web/client-api/src/main/java/io/deephaven/web/client/api/QueryConnectable.java index 8de511fb30f..233bded7ee8 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/api/QueryConnectable.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/QueryConnectable.java @@ -146,9 +146,8 @@ public JsRunnable onLogMessage(JsConsumer callback) { public CancellablePromise startSession(String type) { JsLog.debug("Starting", type, "console session"); LazyPromise promise = new LazyPromise<>(); - final ClientConfiguration config = connection.get().getConfig(); - final Ticket ticket = new Ticket(); - ticket.setTicket(config.newTicketRaw()); + final Tickets config = connection.get().getTickets(); + final Ticket ticket = config.newExportTicket(); final JsRunnable closer = () -> { boolean run = !cancelled.has(ticket); diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/TableTicket.java b/web/client-api/src/main/java/io/deephaven/web/client/api/TableTicket.java index 3b2746b39c2..52d616a50f5 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/api/TableTicket.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/TableTicket.java @@ -7,24 +7,12 @@ import io.deephaven.javascript.proto.dhinternal.arrow.flight.protocol.flight_pb.FlightDescriptor; import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.ticket_pb.Ticket; import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.table_pb.TableReference; -import io.deephaven.web.client.api.console.JsVariableDefinition; /** - * Replacement for TableHandle, wraps up Ticket plus current export state. We only consider the lower bytes for hashing - * (since until we've got millions of tickets it won't matter). + * Replacement for TableHandle, wraps up export tickets plus current export state. We only consider the lower bytes for + * hashing (since until we've got millions of tickets it won't matter). */ public class TableTicket { - public static Ticket createTicket(JsVariableDefinition varDef) { - Ticket ticket = new Ticket(); - ticket.setTicket(varDef.getId()); - return ticket; - } - - public static TableReference createTableRef(JsVariableDefinition varDef) { - TableReference tableRef = new TableReference(); - tableRef.setTicket(createTicket(varDef)); - return tableRef; - } /** * UNKNOWN: 0, PENDING: 1, PUBLISHING: 2, QUEUED: 3, RUNNING: 4, EXPORTED: 5, RELEASED: 6, CANCELLED: 7, FAILED: 8, diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/Tickets.java b/web/client-api/src/main/java/io/deephaven/web/client/api/Tickets.java new file mode 100644 index 00000000000..6096516a417 --- /dev/null +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/Tickets.java @@ -0,0 +1,160 @@ +// +// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending +// +package io.deephaven.web.client.api; + +import elemental2.core.TypedArray; +import elemental2.core.Uint8Array; +import elemental2.dom.DomGlobal; +import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.table_pb.TableReference; +import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.ticket_pb.Ticket; +import io.deephaven.web.client.api.console.JsVariableDefinition; +import jsinterop.annotations.JsMethod; +import jsinterop.annotations.JsPackage; +import jsinterop.base.Js; + +/** + * Single factory for known ticket types. By definition, this cannot be exhaustive, since flight tickets have no + * inherent structure - Deephaven Core only specifies that the first byte will indicate the type of ticket, and later + * bytes will be handled by handlers for that type. Deephaven Core requires only that export tickets be support, but + * also offers application tickets, scope tickets, and shared tickets. Downstream projects may define new ticket types, + * which won't necessarily be understood by this client. + * + * @see io.deephaven.server.session.ExportTicketResolver + * @see io.deephaven.server.appmode.ApplicationTicketResolver + * @see io.deephaven.server.console.ScopeTicketResolver + * @see io.deephaven.server.session.SharedTicketResolver + */ +public class Tickets { + // Prefix for all export tickets + private static final byte EXPORT_PREFIX = 'e'; + // Prefix for all application tickets + private static final byte APPLICATION_PREFIX = 'a'; + // Prefix for all scope tickets + private static final byte SCOPE_PREFIX = 's'; + // Prefix for all shared tickets + private static final byte SHARED_PREFIX = 'h'; + + // Some ticket types use a slash as a delimeter between fields + private static final char TICKET_DELIMITER = '/'; + + /** + * The next number to use when making an export ticket. These values must always be positive, as zero is an invalid + * value, and negative values represent server-created tickets. + */ + private int nextExport = 1; + + public Tickets() {} + + /** + * Utility method to create a ticket from a known-valid base64 encoding of a ticket. + *

+ * Use caution with non-export tickets, the definition may change between calls to the server - they should be + * exported before use. + * + * @param varDef the variable definition to create a ticket from + * @return a ticket with the variable's id as the ticket bytes + */ + public static Ticket createTicket(JsVariableDefinition varDef) { + Ticket ticket = new Ticket(); + ticket.setTicket(varDef.getId()); + return ticket; + } + + /** + * Utility method to create a ticket wrapped in a TableReference from a known-valid base64 encoding of a ticket. + *

+ * Use caution with non-export tickets, the definition may change between calls to the server - they should be + * exported before use. + * + * @param varDef the variable definition to create a ticket from + * @return a table reference with the variable's id as the ticket bytes + */ + + public static TableReference createTableRef(JsVariableDefinition varDef) { + TableReference tableRef = new TableReference(); + tableRef.setTicket(createTicket(varDef)); + return tableRef; + } + + public static void validateScopeOrApplicationTicketBase64(String base64Bytes) { + String bytes = DomGlobal.atob(base64Bytes); + if (bytes.length() > 2) { + String prefix = bytes.substring(0, 2); + if ((prefix.charAt(0) == SCOPE_PREFIX || prefix.charAt(0) == APPLICATION_PREFIX) + && prefix.charAt(1) == TICKET_DELIMITER) { + return; + } + } + throw new IllegalArgumentException("Cannot create a VariableDefinition from a non-scope ticket"); + } + + /** + * Provides the next export id for the current session as a ticket. + * + * @return a new ticket with an export id that hasn't previously been used for this session + */ + public Ticket newExportTicket() { + Ticket ticket = new Ticket(); + ticket.setTicket(newExportTicketRaw()); + return ticket; + } + + /** + * Provides the next export id for the current session. + * + * @return the next export id + */ + public int newTicketInt() { + if (nextExport == Integer.MAX_VALUE) { + throw new IllegalStateException("Ran out of tickets!"); + } + + return nextExport++; + } + + private Uint8Array newExportTicketRaw() { + final int exportId = newTicketInt(); + final double[] dest = new double[5]; + dest[0] = EXPORT_PREFIX; + dest[1] = (byte) exportId; + dest[2] = (byte) (exportId >>> 8); + dest[3] = (byte) (exportId >>> 16); + dest[4] = (byte) (exportId >>> 24); + + final Uint8Array bytes = new Uint8Array(5); + bytes.set(dest); + return bytes; + } + + /** + * Provides the next export id for the current session as a table ticket. + * + * @return a new table ticket with an export id that hasn't previously been used for this session + */ + public TableTicket newTableTicket() { + return new TableTicket(newExportTicketRaw()); + } + + /** + * Creates a shared ticket from the provided array of bytes. + *

+ * Use caution with non-export tickets, the definition may change between calls to the server - they should be + * exported before use. + * + * @param array array of bytes to populate the ticket with + * @return a new shared ticket + */ + public Ticket sharedTicket(TypedArray.SetArrayUnionType array) { + int length = Js.asArrayLike(array).getLength(); + Uint8Array bytesWithPrefix = new Uint8Array(length + 2); + // Add the shared ticket prefix at the start of the provided value + bytesWithPrefix.setAt(0, (double) SHARED_PREFIX); + bytesWithPrefix.setAt(1, (double) TICKET_DELIMITER); + bytesWithPrefix.set(array, 2); + + Ticket ticket = new Ticket(); + ticket.setTicket(bytesWithPrefix); + return ticket; + } +} 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 3c703feba90..1e94cacd940 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 @@ -162,7 +162,7 @@ private enum State { private final QueryConnectable info; private final ClientRpcOptions options = ClientRpcOptions.create(); - private final ClientConfiguration config; + private final Tickets tickets; private final ReconnectState newSessionReconnect; private final TableReviver reviver; // un-finished fetch operations - these can fail on connection issues, won't be attempted again @@ -208,7 +208,7 @@ private enum State { public WorkerConnection(QueryConnectable info) { this.info = info; - this.config = new ClientConfiguration(); + this.tickets = new Tickets(); state = State.Connecting; this.reviver = new TableReviver(this); sessionServiceClient = info.createClient(SessionServiceClient::new); @@ -674,12 +674,12 @@ public Promise getTable(JsVariableDefinition varDef, @Nullable Boolean // TODO (deephaven-core#188): eliminate this branch by applying preview cols before subscribing if (applyPreviewColumns == null || applyPreviewColumns) { ApplyPreviewColumnsRequest req = new ApplyPreviewColumnsRequest(); - req.setSourceId(TableTicket.createTableRef(varDef)); + req.setSourceId(Tickets.createTableRef(varDef)); req.setResultId(cts.getHandle().makeTicket()); tableServiceClient.applyPreviewColumns(req, metadata, c::apply); } else { FetchTableRequest req = new FetchTableRequest(); - req.setSourceId(TableTicket.createTableRef(varDef)); + req.setSourceId(Tickets.createTableRef(varDef)); req.setResultId(cts.getHandle().makeTicket()); tableServiceClient.fetchTable(req, metadata, c::apply); } @@ -793,26 +793,16 @@ public Promise shareObject(ServerObject object, SharedEx }).then(ignore -> Promise.resolve(sharedTicketBytes)); } - private static Ticket sharedTicketFromStringOrBytes(SharedExportBytesUnion sharedTicketBytes) { - Ticket ticket = new Ticket(); - final int length; + private Ticket sharedTicketFromStringOrBytes(SharedExportBytesUnion sharedTicketBytes) { final TypedArray.SetArrayUnionType array; if (sharedTicketBytes.isString()) { byte[] arr = sharedTicketBytes.toString().getBytes(StandardCharsets.UTF_8); - length = arr.length; array = TypedArray.SetArrayUnionType.of(arr); } else { Uint8Array bytes = (Uint8Array) sharedTicketBytes; - length = bytes.length; array = TypedArray.SetArrayUnionType.of(bytes); } - Uint8Array bytesWithPrefix = new Uint8Array(length + 2); - // Add the shared ticket prefix at the start of the provided value - bytesWithPrefix.setAt(0, (double) 'h'); - bytesWithPrefix.setAt(1, (double) '/'); - bytesWithPrefix.set(array, 2); - ticket.setTicket(bytesWithPrefix); - return ticket; + return tickets.sharedTicket(array); } public Promise getSharedObject(SharedExportBytesUnion sharedExportBytes, String type) { @@ -840,7 +830,7 @@ public Promise getSharedObject(SharedExportBytesUnion sharedExportBytes, Stri } TypedTicket result = new TypedTicket(); - result.setTicket(getConfig().newTicket()); + result.setTicket(getTickets().newExportTicket()); result.setType(type); ExportRequest request = new ExportRequest(); @@ -936,7 +926,7 @@ public Promise whenServerReady(String operationName) { } private TicketAndPromise exportScopeTicket(JsVariableDefinition varDef) { - Ticket ticket = getConfig().newTicket(); + Ticket ticket = getTickets().newExportTicket(); return new TicketAndPromise<>(ticket, whenServerReady("exportScopeTicket").then(server -> { ExportRequest req = new ExportRequest(); req.setSourceId(createTypedTicket(varDef).getTicket()); @@ -985,7 +975,7 @@ private static FetchObjectResponse makeFigureFetchResponse(JsWidget widget) { private TypedTicket createTypedTicket(JsVariableDefinition varDef) { TypedTicket typedTicket = new TypedTicket(); - typedTicket.setTicket(TableTicket.createTicket(varDef)); + typedTicket.setTicket(Tickets.createTicket(varDef)); typedTicket.setType(varDef.getType()); return typedTicket; } @@ -1063,7 +1053,7 @@ public BrowserHeaders metadata() { } public BiDiStream.Factory streamFactory() { - return new BiDiStream.Factory<>(info.supportsClientStreaming(), this::metadata, config::newTicketInt); + return new BiDiStream.Factory<>(info.supportsClientStreaming(), this::metadata, tickets::newTicketInt); } public Promise newTable(String[] columnNames, String[] types, Object[][] data, String userTimeZone, @@ -1256,10 +1246,6 @@ private JsTable getFirstByHandle(TableTicket handle) { return null; } - private TableTicket newHandle() { - return new TableTicket(config.newTicketRaw()); - } - public RequestBatcher getBatcher(JsTable table) { // LATER: consider a global client.batch(()=>{}) method which causes all table statements to be batched // together. @@ -1294,7 +1280,8 @@ public ClientTableState newStateFromUnsolicitedTable(ExportedTableCreationRespon } public ClientTableState newState(JsTableFetch fetcher, String fetchSummary) { - return cache.create(newHandle(), handle -> new ClientTableState(this, handle, fetcher, fetchSummary)); + return cache.create(tickets.newTableTicket(), + handle -> new ClientTableState(this, handle, fetcher, fetchSummary)); } /** @@ -1305,13 +1292,13 @@ public ClientTableState newState(JsTableFetch fetcher, String fetchSummary) { * TODO: consider a fetch timeout. */ public Promise newState(HasEventHandling failHandler, JsTableFetch fetcher, String fetchSummary) { - final TableTicket handle = newHandle(); + final TableTicket handle = tickets.newTableTicket(); final ClientTableState s = cache.create(handle, h -> new ClientTableState(this, h, fetcher, fetchSummary)); return s.refetch(failHandler, metadata); } public ClientTableState newState(ClientTableState from, TableConfig to) { - return newState(from, to, newHandle()); + return newState(from, to, tickets.newTableTicket()); } public ClientTableState newState(ClientTableState from, TableConfig to, TableTicket handle) { @@ -1386,8 +1373,8 @@ public boolean isUsable() { return false; } - public ClientConfiguration getConfig() { - return config; + public Tickets getTickets() { + return tickets; } public ConfigValue getServerConfigValue(String key) { diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/console/JsVariableDefinition.java b/web/client-api/src/main/java/io/deephaven/web/client/api/console/JsVariableDefinition.java index ddc820c3503..26f648c1cda 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/api/console/JsVariableDefinition.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/console/JsVariableDefinition.java @@ -7,6 +7,7 @@ import com.vertispan.tsdefs.annotations.TsName; import com.vertispan.tsdefs.annotations.TsTypeRef; import io.deephaven.javascript.proto.dhinternal.io.deephaven_core.proto.application_pb.FieldInfo; +import io.deephaven.web.client.api.Tickets; import jsinterop.annotations.JsProperty; /** @@ -28,11 +29,7 @@ public class JsVariableDefinition { private final String applicationName; public JsVariableDefinition(String type, String title, String id, String description) { - // base64('s/' + str) starts with 'cy8' or 'cy9' - // base64('a/' + str) starts with 'YS8' or 'YS9' - if (!id.startsWith("cy") && !id.startsWith("YS")) { - throw new IllegalArgumentException("Cannot create a VariableDefinition from a non-scope ticket"); - } + Tickets.validateScopeOrApplicationTicketBase64(id); this.type = type; this.title = title == null ? JS_UNAVAILABLE : title; this.id = id; diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/input/JsInputTable.java b/web/client-api/src/main/java/io/deephaven/web/client/api/input/JsInputTable.java index 26ebd6d5575..63e90534e44 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/api/input/JsInputTable.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/input/JsInputTable.java @@ -238,7 +238,7 @@ public Promise deleteTables(JsTable[] tablesToDelete) { failureToReport = Promise.resolve((Object) null); } else { // view the only table - ticketToDelete = table.getConnection().getConfig().newTicket(); + ticketToDelete = table.getConnection().getTickets().newExportTicket(); cleanups.add(() -> table.getConnection().releaseTicket(ticketToDelete)); SelectOrUpdateRequest view = new SelectOrUpdateRequest(); @@ -250,7 +250,7 @@ public Promise deleteTables(JsTable[] tablesToDelete) { } } else { // there is more than one table here, construct a merge after making a view of each table - ticketToDelete = table.getConnection().getConfig().newTicket(); + ticketToDelete = table.getConnection().getTickets().newExportTicket(); cleanups.add(() -> table.getConnection().releaseTicket(ticketToDelete)); BatchTableRequest batch = new BatchTableRequest(); 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 5ced188ea4e..369ccafe385 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 @@ -306,7 +306,7 @@ private TicketAndPromise prepareFilter() { if (nextFilters.isEmpty()) { return new TicketAndPromise<>(widget.getTicket(), connection); } - Ticket ticket = connection.getConfig().newTicket(); + Ticket ticket = connection.getTickets().newExportTicket(); filteredTable = new TicketAndPromise<>(ticket, Callbacks.grpcUnaryPromise(c -> { HierarchicalTableApplyRequest applyFilter = new HierarchicalTableApplyRequest(); @@ -326,7 +326,7 @@ private TicketAndPromise prepareSort(TicketAndPromise prevTicket) { if (nextSort.isEmpty()) { return prevTicket; } - Ticket ticket = connection.getConfig().newTicket(); + Ticket ticket = connection.getTickets().newExportTicket(); sortedTable = new TicketAndPromise<>(ticket, Callbacks.grpcUnaryPromise(c -> { HierarchicalTableApplyRequest applyFilter = new HierarchicalTableApplyRequest(); applyFilter.setSortsList(nextSort.stream().map(Sort::makeDescriptor).toArray( @@ -358,7 +358,7 @@ private TicketAndPromise makeView(TicketAndPromise prevTicket) { if (viewTicket != null) { return viewTicket; } - Ticket ticket = connection.getConfig().newTicket(); + Ticket ticket = connection.getTickets().newExportTicket(); Promise keyTable = makeKeyTable(); viewTicket = new TicketAndPromise<>(ticket, Callbacks.grpcUnaryPromise(c -> { HierarchicalTableViewRequest viewRequest = new HierarchicalTableViewRequest(); diff --git a/web/client-api/src/main/java/io/deephaven/web/client/api/widget/JsWidgetExportedObject.java b/web/client-api/src/main/java/io/deephaven/web/client/api/widget/JsWidgetExportedObject.java index 5c7fdf4c7ba..6dfa7741d52 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/api/widget/JsWidgetExportedObject.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/api/widget/JsWidgetExportedObject.java @@ -99,7 +99,7 @@ public TypedTicket typedTicket() { */ @JsMethod public Promise reexport() { - Ticket reexportedTicket = connection.getConfig().newTicket(); + Ticket reexportedTicket = connection.getTickets().newExportTicket(); // Future optimization - we could "race" these by running the export in the background, to avoid // an extra round trip. From 87e7b21b66721b4669940786853686b753b7ccf5 Mon Sep 17 00:00:00 2001 From: Colin Alworth Date: Fri, 17 Jan 2025 08:40:02 -0600 Subject: [PATCH 7/7] Fixed shared type bytes union members --- .../deephaven/web/client/api/WorkerConnection.java | 4 ++-- .../web/client/ide/SharedExportBytesUnion.java | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) 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 1e94cacd940..bd4591d3386 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 @@ -796,10 +796,10 @@ public Promise shareObject(ServerObject object, SharedEx private Ticket sharedTicketFromStringOrBytes(SharedExportBytesUnion sharedTicketBytes) { final TypedArray.SetArrayUnionType array; if (sharedTicketBytes.isString()) { - byte[] arr = sharedTicketBytes.toString().getBytes(StandardCharsets.UTF_8); + byte[] arr = sharedTicketBytes.asString().getBytes(StandardCharsets.UTF_8); array = TypedArray.SetArrayUnionType.of(arr); } else { - Uint8Array bytes = (Uint8Array) sharedTicketBytes; + Uint8Array bytes = sharedTicketBytes.asUint8Array(); array = TypedArray.SetArrayUnionType.of(bytes); } return tickets.sharedTicket(array); diff --git a/web/client-api/src/main/java/io/deephaven/web/client/ide/SharedExportBytesUnion.java b/web/client-api/src/main/java/io/deephaven/web/client/ide/SharedExportBytesUnion.java index de0c1c79965..b989148840d 100644 --- a/web/client-api/src/main/java/io/deephaven/web/client/ide/SharedExportBytesUnion.java +++ b/web/client-api/src/main/java/io/deephaven/web/client/ide/SharedExportBytesUnion.java @@ -4,6 +4,7 @@ package io.deephaven.web.client.ide; import com.vertispan.tsdefs.annotations.TsUnion; +import com.vertispan.tsdefs.annotations.TsUnionMember; import elemental2.core.Uint8Array; import jsinterop.annotations.JsOverlay; import jsinterop.annotations.JsPackage; @@ -27,4 +28,16 @@ default boolean isString() { default boolean isUint8Array() { return this instanceof Uint8Array; } + + @TsUnionMember + @JsOverlay + default String asString() { + return Js.cast(this); + } + + @TsUnionMember + @JsOverlay + default Uint8Array asUint8Array() { + return (Uint8Array) this; + } }