diff --git a/tensorboard/components/experimental/plugin_lib/BUILD b/tensorboard/components/experimental/plugin_lib/BUILD
new file mode 100644
index 0000000000..d4528b9095
--- /dev/null
+++ b/tensorboard/components/experimental/plugin_lib/BUILD
@@ -0,0 +1,32 @@
+package(default_visibility = ["//tensorboard:internal"])
+
+load("//tensorboard/defs:web.bzl", "tf_web_library")
+
+licenses(["notice"]) # Apache 2.0
+
+tf_web_library(
+ name = "guest_internals",
+ srcs = [
+ "plugin-guest.ts",
+ ],
+ path = "/tf-plugin-lib",
+ deps = [
+ "//tensorboard/components/experimental/plugin_util:message",
+ ],
+)
+
+# TODO(psybuzz): figure out how this tf_web_library can be used to create
+# maybe a NPM package.
+tf_web_library(
+ name = "plugin_lib",
+ srcs = [
+ "runs.ts",
+ "tf-plugin-lib.html",
+ ],
+ path = "/tf-plugin-lib",
+ visibility = ["//visibility:public"],
+ deps = [
+ ":guest_internals",
+ "//tensorboard/components/experimental/plugin_util:message",
+ ],
+)
diff --git a/tensorboard/components/experimental/plugin_lib/plugin-guest.ts b/tensorboard/components/experimental/plugin_lib/plugin-guest.ts
new file mode 100644
index 0000000000..8bf480f655
--- /dev/null
+++ b/tensorboard/components/experimental/plugin_lib/plugin-guest.ts
@@ -0,0 +1,55 @@
+/* Copyright 2019 The TensorFlow Authors. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+==============================================================================*/
+namespace tb_plugin.lib.DO_NOT_USE_INTERNAL {
+ /**
+ * This code is part of a public bundle provided to plugin authors,
+ * and runs within an IFrame to setup communication with TensorBoard's frame.
+ */
+ if (!window.parent) {
+ throw Error(
+ 'The library must run within a TensorBoard iframe-based plugin.'
+ );
+ }
+
+ const channel = new MessageChannel();
+ const ipc = new IPC(channel.port1);
+ channel.port1.start();
+
+ const VERSION = 'experimental';
+ window.parent.postMessage(`${VERSION}.bootstrap`, '*', [channel.port2]);
+
+ // Only export for testability.
+ export const _guestIPC = ipc;
+
+ /**
+ * Sends a message to the parent frame.
+ * @return Promise that resolves with a payload from parent in response to this message.
+ *
+ * @example
+ * const someList = await sendMessage('v1.some.type.parent.understands');
+ * // do fun things with someList.
+ */
+ export const sendMessage = _guestIPC.sendMessage.bind(_guestIPC);
+
+ /**
+ * Subscribes a callback to a message with particular type.
+ */
+ export const listen = _guestIPC.listen.bind(_guestIPC);
+
+ /**
+ * Unsubscribes a callback to a message.
+ */
+ export const unlisten = _guestIPC.unlisten.bind(_guestIPC);
+} // namespace tb_plugin.lib.DO_NOT_USE_INTERNAL
diff --git a/tensorboard/components/experimental/plugin_util/test/iframe.ts b/tensorboard/components/experimental/plugin_lib/runs.ts
similarity index 65%
rename from tensorboard/components/experimental/plugin_util/test/iframe.ts
rename to tensorboard/components/experimental/plugin_lib/runs.ts
index c1c979687c..e672d67806 100644
--- a/tensorboard/components/experimental/plugin_util/test/iframe.ts
+++ b/tensorboard/components/experimental/plugin_lib/runs.ts
@@ -12,8 +12,17 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
+namespace tb_plugin.lib.runs {
+ export async function getRuns() {
+ return tb_plugin.lib.DO_NOT_USE_INTERNAL.sendMessage(
+ 'experimental.GetRuns'
+ );
+ }
-import {sendMessage, listen, unlisten, _guestIPC} from '../plugin-guest.js';
-
-const win = window as any;
-win.test = {sendMessage, listen, unlisten, _guestIPC};
+ export function setOnRunsChanged(callback: (runs: string[]) => void | void) {
+ return tb_plugin.lib.DO_NOT_USE_INTERNAL.listen(
+ 'experimental.RunsChanged',
+ callback
+ );
+ }
+}
diff --git a/tensorboard/components/experimental/plugin_lib/test/BUILD b/tensorboard/components/experimental/plugin_lib/test/BUILD
new file mode 100644
index 0000000000..5493fc842b
--- /dev/null
+++ b/tensorboard/components/experimental/plugin_lib/test/BUILD
@@ -0,0 +1,31 @@
+package(
+ default_testonly = True,
+ default_visibility = ["//tensorboard:internal"],
+)
+
+load("//tensorboard/defs:web.bzl", "tf_web_library", "tf_web_test")
+
+licenses(["notice"]) # Apache 2.0
+
+tf_web_test(
+ name = "test",
+ src = "/tf-plugin-lib/test/test.html",
+ web_library = ":test_web_library",
+)
+
+tf_web_library(
+ name = "test_web_library",
+ testonly = True,
+ srcs = [
+ "test.html",
+ "test.ts",
+ "testable-iframe.html",
+ ],
+ path = "/tf-plugin-lib/test",
+ deps = [
+ "//tensorboard/components/experimental/plugin_lib",
+ "//tensorboard/components/experimental/plugin_util:plugin_host",
+ "//tensorboard/components/tf_backend",
+ "//tensorboard/components/tf_imports:web_component_tester",
+ ],
+)
diff --git a/tensorboard/components/experimental/plugin_lib/test/test.html b/tensorboard/components/experimental/plugin_lib/test/test.html
new file mode 100644
index 0000000000..69a9862a1c
--- /dev/null
+++ b/tensorboard/components/experimental/plugin_lib/test/test.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
diff --git a/tensorboard/components/experimental/plugin_lib/test/test.ts b/tensorboard/components/experimental/plugin_lib/test/test.ts
new file mode 100644
index 0000000000..b6e79f0d58
--- /dev/null
+++ b/tensorboard/components/experimental/plugin_lib/test/test.ts
@@ -0,0 +1,91 @@
+/* Copyright 2019 The TensorFlow Authors. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+==============================================================================*/
+
+async function createIframe(): Promise {
+ return new Promise((resolve) => {
+ const iframe = document.createElement('iframe') as HTMLIFrameElement;
+ document.body.appendChild(iframe);
+ iframe.src = './testable-iframe.html';
+ iframe.onload = () => resolve(iframe);
+ });
+}
+
+describe('plugin lib integration', () => {
+ const {expect} = chai;
+
+ beforeEach(async function() {
+ this.sandbox = sinon.sandbox.create({useFakeServer: true});
+ this.sandbox.server.respondImmediately = true;
+ this.iframe = await createIframe();
+ this.lib = (this.iframe.contentWindow as any).plugin_lib;
+ });
+
+ afterEach(function() {
+ document.body.removeChild(this.iframe);
+ this.sandbox.restore();
+ });
+
+ describe('tb_plugin.lib.run', () => {
+ describe('#getRuns', () => {
+ it('returns list of runs', async function() {
+ this.sandbox
+ .stub(tf_backend.runsStore, 'getRuns')
+ .returns(['foo', 'bar', 'baz']);
+
+ const runs = await this.lib.runs.getRuns();
+ expect(runs).to.deep.equal(['foo', 'bar', 'baz']);
+ });
+ });
+ describe('#setOnRunsChanged', () => {
+ it('lets plugins subscribe to runs change', async function() {
+ const runsChanged = this.sandbox.stub();
+ const promise = new Promise((resolve) => {
+ this.lib.runs.setOnRunsChanged(resolve);
+ }).then(runsChanged);
+ this.sandbox.server.respondWith([
+ 200,
+ {'Content-Type': 'application/json'},
+ '["foo", "bar"]',
+ ]);
+
+ await tf_backend.runsStore.refresh();
+ await promise;
+
+ expect(runsChanged).to.have.been.calledOnce;
+ expect(runsChanged).to.have.been.calledWith(['foo', 'bar']);
+ });
+ it('lets plugins unsubscribe to runs change', async function() {
+ const runsChanged = this.sandbox.stub();
+ const promise = new Promise((resolve) => {
+ this.lib.runs.setOnRunsChanged(resolve);
+ }).then(runsChanged);
+ this.lib.runs.setOnRunsChanged();
+ this.sandbox.server.respondWith([
+ 200,
+ {'Content-Type': 'application/json'},
+ '["foo", "bar"]',
+ ]);
+
+ await tf_backend.runsStore.refresh();
+
+ // Await another message to ensure the iframe processed the next message
+ // (if any).
+ await this.lib.DO_NOT_USE_INTERNAL.sendMessage('foo');
+
+ expect(runsChanged).to.not.have.been.called;
+ });
+ });
+ });
+});
diff --git a/tensorboard/components/experimental/plugin_lib/test/testable-iframe.html b/tensorboard/components/experimental/plugin_lib/test/testable-iframe.html
new file mode 100644
index 0000000000..a71873734a
--- /dev/null
+++ b/tensorboard/components/experimental/plugin_lib/test/testable-iframe.html
@@ -0,0 +1,20 @@
+
+
+
diff --git a/tensorboard/components/experimental/plugin_lib/tf-plugin-lib.html b/tensorboard/components/experimental/plugin_lib/tf-plugin-lib.html
new file mode 100644
index 0000000000..ed9aa45eef
--- /dev/null
+++ b/tensorboard/components/experimental/plugin_lib/tf-plugin-lib.html
@@ -0,0 +1,20 @@
+
+
+
+
+
diff --git a/tensorboard/components/experimental/plugin_util/BUILD b/tensorboard/components/experimental/plugin_util/BUILD
index 622dfeef6e..ccd6969f3b 100644
--- a/tensorboard/components/experimental/plugin_util/BUILD
+++ b/tensorboard/components/experimental/plugin_util/BUILD
@@ -5,35 +5,35 @@ load("//tensorboard/defs:web.bzl", "tf_web_library")
licenses(["notice"]) # Apache 2.0
tf_web_library(
- name = "plugin_host",
+ name = "message",
srcs = [
- "plugin-host.html",
- "plugin-host.ts",
- ],
- path = "/tf-plugin",
- deps = [
- ":plugin_lib",
- "//tensorboard/components/tf_backend",
+ "message.html",
+ "message.ts",
],
+ path = "/tf-plugin-util",
)
tf_web_library(
- name = "plugin_lib",
+ name = "host_internals",
srcs = [
- "message.ts",
+ "plugin-host-ipc.html",
+ "plugin-host-ipc.ts",
+ ],
+ path = "/tf-plugin-util",
+ deps = [
+ ":message",
],
- path = "/tf-plugin",
)
tf_web_library(
- name = "plugin_guest",
+ name = "plugin_host",
srcs = [
- "plugin-guest.html",
- "plugin-guest.ts",
+ "plugin-host.html",
+ "runs-host-impl.ts",
],
- path = "/tf-plugin",
- visibility = ["//visibility:public"],
+ path = "/tf-plugin-util",
deps = [
- ":plugin_lib",
+ ":host_internals",
+ "//tensorboard/components/tf_backend",
],
)
diff --git a/tensorboard/components/experimental/plugin_util/plugin-guest.html b/tensorboard/components/experimental/plugin_util/message.html
similarity index 94%
rename from tensorboard/components/experimental/plugin_util/plugin-guest.html
rename to tensorboard/components/experimental/plugin_util/message.html
index 807d96f6e7..bd98b715f5 100644
--- a/tensorboard/components/experimental/plugin_util/plugin-guest.html
+++ b/tensorboard/components/experimental/plugin_util/message.html
@@ -14,5 +14,4 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
diff --git a/tensorboard/components/experimental/plugin_util/message.ts b/tensorboard/components/experimental/plugin_util/message.ts
index 805f2f2752..bc132b8919 100644
--- a/tensorboard/components/experimental/plugin_util/message.ts
+++ b/tensorboard/components/experimental/plugin_util/message.ts
@@ -17,100 +17,101 @@ limitations under the License.
* This file defines utilities shared by TensorBoard (plugin host) and the
* dynamic plugin library, used by plugin authors.
*/
-
-export type PayloadType =
- | null
- | undefined
- | string
- | string[]
- | boolean
- | boolean[]
- | number
- | number[]
- | object
- | object[];
-
-export interface Message {
- type: string;
- id: number;
- payload: PayloadType;
- error: string | null;
- isReply: boolean;
-}
-
-export type MessageType = string;
-export type MessageCallback = (payload: any) => any;
-
-interface PromiseResolver {
- resolve: (data: any) => void;
- reject: (error: Error) => void;
-}
-
-export class IPC {
- private id = 0;
- private readonly responseWaits = new Map();
- private readonly listeners = new Map();
-
- constructor(private port: MessagePort) {
- this.port.addEventListener('message', (event) => this.onMessage(event));
+namespace tb_plugin.lib.DO_NOT_USE_INTERNAL {
+ export type PayloadType =
+ | null
+ | undefined
+ | string
+ | string[]
+ | boolean
+ | boolean[]
+ | number
+ | number[]
+ | object
+ | object[];
+
+ export interface Message {
+ type: string;
+ id: number;
+ payload: PayloadType;
+ error: string | null;
+ isReply: boolean;
}
- listen(type: MessageType, callback: MessageCallback) {
- this.listeners.set(type, callback);
- }
+ export type MessageType = string;
+ export type MessageCallback = (payload: any) => any;
- unlisten(type: MessageType) {
- this.listeners.delete(type);
+ interface PromiseResolver {
+ resolve: (data: any) => void;
+ reject: (error: Error) => void;
}
- private async onMessage(event: MessageEvent) {
- const message = JSON.parse(event.data) as Message;
- const callback = this.listeners.get(message.type);
-
- if (message.isReply) {
- if (!this.responseWaits.has(message.id)) return;
- const {id, payload, error} = message;
- const {resolve, reject} = this.responseWaits.get(id);
- this.responseWaits.delete(id);
- if (error) {
- reject(new Error(error));
- } else {
- resolve(payload);
- }
- return;
+ export class IPC {
+ private id = 0;
+ private readonly responseWaits = new Map();
+ private readonly listeners = new Map();
+
+ constructor(private port: MessagePort) {
+ this.port.addEventListener('message', (event) => this.onMessage(event));
+ }
+
+ listen(type: MessageType, callback: MessageCallback) {
+ this.listeners.set(type, callback);
+ }
+
+ unlisten(type: MessageType) {
+ this.listeners.delete(type);
}
- let payload = null;
- let error = null;
- if (this.listeners.has(message.type)) {
+ private async onMessage(event: MessageEvent) {
+ const message = JSON.parse(event.data) as Message;
const callback = this.listeners.get(message.type);
- try {
- const result = await callback(message.payload);
- payload = result;
- } catch (e) {
- error = e;
+
+ if (message.isReply) {
+ if (!this.responseWaits.has(message.id)) return;
+ const {id, payload, error} = message;
+ const {resolve, reject} = this.responseWaits.get(id);
+ this.responseWaits.delete(id);
+ if (error) {
+ reject(new Error(error));
+ } else {
+ resolve(payload);
+ }
+ return;
+ }
+
+ let payload = null;
+ let error = null;
+ if (this.listeners.has(message.type)) {
+ const callback = this.listeners.get(message.type);
+ try {
+ const result = await callback(message.payload);
+ payload = result;
+ } catch (e) {
+ error = e;
+ }
}
+ const replyMessage: Message = {
+ type: message.type,
+ id: message.id,
+ payload,
+ error,
+ isReply: true,
+ };
+ this.postMessage(replyMessage);
}
- const replyMessage: Message = {
- type: message.type,
- id: message.id,
- payload,
- error,
- isReply: true,
- };
- this.postMessage(replyMessage);
- }
- private postMessage(message: Message) {
- this.port.postMessage(JSON.stringify(message));
- }
+ private postMessage(message: Message) {
+ this.port.postMessage(JSON.stringify(message));
+ }
- sendMessage(type: MessageType, payload: PayloadType): Promise {
- const id = this.id++;
- const message: Message = {type, id, payload, error: null, isReply: false};
- this.postMessage(message);
- return new Promise((resolve, reject) => {
- this.responseWaits.set(id, {resolve, reject});
- });
+ sendMessage(type: MessageType, payload: PayloadType): Promise {
+ const id = this.id++;
+ const message: Message = {type, id, payload, error: null, isReply: false};
+ this.postMessage(message);
+ return new Promise((resolve, reject) => {
+ this.responseWaits.set(id, {resolve, reject});
+ });
+ }
}
-}
+} // namespace tb_plugin.lib.DO_NOT_USE_INTERNAL
diff --git a/tensorboard/components/experimental/plugin_util/plugin-guest.ts b/tensorboard/components/experimental/plugin_util/plugin-guest.ts
deleted file mode 100644
index 5837473518..0000000000
--- a/tensorboard/components/experimental/plugin_util/plugin-guest.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-/* Copyright 2019 The TensorFlow Authors. All Rights Reserved.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-==============================================================================*/
-import {IPC, Message} from './message.js';
-
-/**
- * This code is part of a public bundle provided to plugin authors,
- * and runs within an IFrame to setup communication with TensorBoard's frame.
- */
-if (!window.parent) {
- throw Error('The library must run within a TensorBoard iframe-based plugin.');
-}
-
-const channel = new MessageChannel();
-const ipc = new IPC(channel.port1);
-channel.port1.start();
-
-const VERSION = 'experimental';
-window.parent.postMessage(`${VERSION}.bootstrap`, '*', [channel.port2]);
-
-// Only export for testability.
-export const _guestIPC = ipc;
-
-/**
- * Sends a message to the parent frame.
- * @return Promise that resolves with a payload from parent in response to this message.
- *
- * @example
- * const someList = await sendMessage('v1.some.type.parent.understands');
- * // do fun things with someList.
- */
-export const sendMessage = _guestIPC.sendMessage.bind(_guestIPC);
-
-/**
- * Subscribes a callback to a message with particular type.
- */
-export const listen = _guestIPC.listen.bind(_guestIPC);
-
-/**
- * Unsubscribes a callback to a message.
- */
-export const unlisten = _guestIPC.unlisten.bind(_guestIPC);
diff --git a/tensorboard/components/experimental/plugin_util/plugin-host-ipc.html b/tensorboard/components/experimental/plugin_util/plugin-host-ipc.html
new file mode 100644
index 0000000000..4f0bcbe83f
--- /dev/null
+++ b/tensorboard/components/experimental/plugin_util/plugin-host-ipc.html
@@ -0,0 +1,18 @@
+
+
+
diff --git a/tensorboard/components/experimental/plugin_util/plugin-host-ipc.ts b/tensorboard/components/experimental/plugin_util/plugin-host-ipc.ts
new file mode 100644
index 0000000000..8618cdd22c
--- /dev/null
+++ b/tensorboard/components/experimental/plugin_util/plugin-host-ipc.ts
@@ -0,0 +1,96 @@
+/* Copyright 2019 The TensorFlow Authors. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+==============================================================================*/
+namespace tb_plugin.host {
+ const portIPCs = new Set();
+ const VERSION = 'experimental';
+ const listeners = new Map<
+ lib.DO_NOT_USE_INTERNAL.MessageType,
+ lib.DO_NOT_USE_INTERNAL.MessageCallback
+ >();
+
+ // TODO(@psybuzz): replace this and the port cleanup logic in broadcast() with
+ // a MutationObserver to notify us when iframes disconnect.
+ const ipcToFrame = new Map();
+
+ // The initial Window-level listener is needed to bootstrap only.
+ // All further communication is done over MessagePorts.
+ window.addEventListener('message', (event) => {
+ if (event.data !== `${VERSION}.bootstrap`) return;
+ const port = event.ports[0];
+ if (!port) return;
+ const frame = event.source ? event.source.frameElement : null;
+ if (!frame) return;
+ onBootstrap(port, frame as HTMLIFrameElement);
+ });
+
+ function onBootstrap(port: MessagePort, frame: HTMLIFrameElement) {
+ const portIPC = new lib.DO_NOT_USE_INTERNAL.IPC(port);
+ portIPCs.add(portIPC);
+ ipcToFrame.set(portIPC, frame);
+ port.start();
+
+ for (const [type, callback] of listeners) {
+ portIPC.listen(type, callback);
+ }
+ }
+
+ /**
+ * Sends a message to all frames. Individual frames decide whether or not to
+ * listen.
+ * @return Promise that resolves with a list of payloads from each plugin's
+ * response (or null) to the message.
+ *
+ * @example
+ * const someList = await broadcast('v1.some.type.guest.understands');
+ * // do fun things with someList.
+ */
+ export function broadcast(
+ type: lib.DO_NOT_USE_INTERNAL.MessageType,
+ payload: lib.DO_NOT_USE_INTERNAL.PayloadType
+ ): Promise {
+ for (const ipc of portIPCs) {
+ if (!ipcToFrame.get(ipc).isConnected) {
+ portIPCs.delete(ipc);
+ ipcToFrame.delete(ipc);
+ }
+ }
+
+ const promises = [...portIPCs].map((ipc) => ipc.sendMessage(type, payload));
+ return Promise.all(promises);
+ }
+
+ /**
+ * Subscribes to messages of a type specified for all frames.
+ */
+ export function listen(
+ type: lib.DO_NOT_USE_INTERNAL.MessageType,
+ callback: lib.DO_NOT_USE_INTERNAL.MessageCallback
+ ) {
+ listeners.set(type, callback);
+ for (const ipc of portIPCs) {
+ ipc.listen(type, callback);
+ }
+ }
+
+ /**
+ * Unsubscribes to messages of a type specified for all frames.
+ */
+ export function unlisten(type: lib.DO_NOT_USE_INTERNAL.MessageType) {
+ listeners.delete(type);
+ for (const ipc of portIPCs) {
+ ipc.unlisten(type);
+ }
+ }
+} // namespace tb_plugin.host
diff --git a/tensorboard/components/experimental/plugin_util/plugin-host.html b/tensorboard/components/experimental/plugin_util/plugin-host.html
index d1b6f3ae89..c9c40556f3 100644
--- a/tensorboard/components/experimental/plugin_util/plugin-host.html
+++ b/tensorboard/components/experimental/plugin_util/plugin-host.html
@@ -14,7 +14,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
+
-
-
+
+
diff --git a/tensorboard/components/experimental/plugin_util/plugin-host.ts b/tensorboard/components/experimental/plugin_util/plugin-host.ts
deleted file mode 100644
index 1de5c554cb..0000000000
--- a/tensorboard/components/experimental/plugin_util/plugin-host.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-/* Copyright 2019 The TensorFlow Authors. All Rights Reserved.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-==============================================================================*/
-import {IPC, MessageType, PayloadType, MessageCallback} from './message.js';
-
-const portIPCs = new Set();
-const VERSION = 'experimental';
-const listeners = new Map();
-
-// TODO(@psybuzz): replace this and the port cleanup logic in broadcast() with
-// a MutationObserver to notify us when iframes disconnect.
-const ipcToFrame = new Map();
-
-// The initial Window-level listener is needed to bootstrap only.
-// All further communication is done over MessagePorts.
-window.addEventListener('message', (event) => {
- if (event.data !== `${VERSION}.bootstrap`) return;
- const port = event.ports[0];
- if (!port) return;
- const frame = event.source ? event.source.frameElement : null;
- if (!frame) return;
- onBootstrap(port, frame as HTMLIFrameElement);
-});
-
-function onBootstrap(port: MessagePort, frame: HTMLIFrameElement) {
- const portIPC = new IPC(port);
- portIPCs.add(portIPC);
- ipcToFrame.set(portIPC, frame);
- port.start();
-
- for (const [type, callback] of listeners) {
- portIPC.listen(type, callback);
- }
-}
-
-function _broadcast(
- type: MessageType,
- payload: PayloadType
-): Promise {
- for (const ipc of portIPCs) {
- if (!ipcToFrame.get(ipc).isConnected) {
- portIPCs.delete(ipc);
- ipcToFrame.delete(ipc);
- }
- }
-
- const promises = [...portIPCs].map((ipc) => ipc.sendMessage(type, payload));
- return Promise.all(promises);
-}
-
-function _listen(type: MessageType, callback: MessageCallback) {
- listeners.set(type, callback);
- for (const ipc of portIPCs) {
- ipc.listen(type, callback);
- }
-}
-
-function _unlisten(type: MessageType) {
- listeners.delete(type);
- for (const ipc of portIPCs) {
- ipc.unlisten(type);
- }
-}
-
-export const broadcast = _broadcast;
-export const listen = _listen;
-export const unlisten = _unlisten;
-
-namespace tf_plugin {
- /**
- * Sends a message to all frames. Individual frames decide whether or not to
- * listen.
- * @return Promise that resolves with a list of payloads from each plugin's
- * response (or null) to the message.
- *
- * @example
- * const someList = await broadcast('v1.some.type.guest.understands');
- * // do fun things with someList.
- */
- export const broadcast = _broadcast;
- /**
- * Subscribes to messages of a type specified for all frames.
- */
- export const listen = _listen;
- /**
- * Unsubscribes to messages of a type specified for all frames.
- */
- export const unlisten = _unlisten;
-} // namespace tf_plugin
diff --git a/tensorboard/components/experimental/plugin_util/runs-host-impl.ts b/tensorboard/components/experimental/plugin_util/runs-host-impl.ts
new file mode 100644
index 0000000000..2d6020421b
--- /dev/null
+++ b/tensorboard/components/experimental/plugin_util/runs-host-impl.ts
@@ -0,0 +1,27 @@
+/* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+==============================================================================*/
+/**
+ * Implements run related plugin APIs.
+ */
+tb_plugin.host.listen('experimental.GetRuns', () => {
+ return tf_backend.runsStore.getRuns();
+});
+
+tf_backend.runsStore.addListener(() => {
+ return tb_plugin.host.broadcast(
+ 'experimental.RunsChanged',
+ tf_backend.runsStore.getRuns()
+ );
+});
diff --git a/tensorboard/components/experimental/plugin_util/test/BUILD b/tensorboard/components/experimental/plugin_util/test/BUILD
index eea69f1d29..e598af9bd5 100644
--- a/tensorboard/components/experimental/plugin_util/test/BUILD
+++ b/tensorboard/components/experimental/plugin_util/test/BUILD
@@ -4,56 +4,25 @@ package(
)
load("//tensorboard/defs:web.bzl", "tf_web_library", "tf_web_test")
-load("//tensorboard/defs:vulcanize.bzl", "tensorboard_html_binary")
licenses(["notice"]) # Apache 2.0
tf_web_test(
name = "test",
- src = "/tf-plugin/test/test_binary.html",
+ src = "/tf-plugin-util/test/tests.html",
web_library = ":test_web_library",
)
-# HACK: specifying tensorboard_html_binary on tf_web_test causes certain
-# environment to throw exception but wrapping tensorbard_html_binary with
-# tf_web_library seems to be okay.
tf_web_library(
name = "test_web_library",
- srcs = [
- ":test_binary.html",
- ],
- path = "/tf-plugin/test",
- deps = [
- ":test_lib",
- "//tensorboard/components/tf_imports:web_component_tester",
- ],
-)
-
-tensorboard_html_binary(
- name = "test_binary",
- # Disable advanced optimization to prevent check for WebComponentTester
- # gobals.
- compilation_level = "SIMPLE",
- # Requires for compiling `import`s away.
- compile = True,
- input_path = "/tf-plugin/test/tests.html",
- output_path = "/tf-plugin/test/test_binary.html",
- deps = [
- ":test_lib",
- ],
-)
-
-tf_web_library(
- name = "test_lib",
srcs = [
"iframe.html",
- "iframe.ts",
"plugin-test.ts",
"tests.html",
],
- path = "/tf-plugin/test",
+ path = "/tf-plugin-util/test",
deps = [
- "//tensorboard/components/experimental/plugin_util:plugin_guest",
+ "//tensorboard/components/experimental/plugin_lib:plugin_lib",
"//tensorboard/components/experimental/plugin_util:plugin_host",
"//tensorboard/components/tf_imports:web_component_tester",
],
diff --git a/tensorboard/components/experimental/plugin_util/test/iframe.html b/tensorboard/components/experimental/plugin_util/test/iframe.html
index 9aff72251d..d0e75f91e2 100644
--- a/tensorboard/components/experimental/plugin_util/test/iframe.html
+++ b/tensorboard/components/experimental/plugin_util/test/iframe.html
@@ -1,4 +1,3 @@
-
-
-
-
+
+
+
diff --git a/tensorboard/components/experimental/plugin_util/test/plugin-test.ts b/tensorboard/components/experimental/plugin_util/test/plugin-test.ts
index b56a30f1d8..ed663b0711 100644
--- a/tensorboard/components/experimental/plugin_util/test/plugin-test.ts
+++ b/tensorboard/components/experimental/plugin_util/test/plugin-test.ts
@@ -12,10 +12,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
-import * as pluginHost from '../plugin-host.js';
-import {Message} from '../message.js';
-namespace tf_plugin.test {
+namespace tb_plugin.lib.DO_NOT_USE_INTERNAL {
const {expect} = chai;
const template = document.getElementById(
'iframe-template'
@@ -60,7 +58,7 @@ namespace tf_plugin.test {
this.destUnlisten = this.guestWindow.test.unlisten;
this.destSendMessage = this.guestWindow.test.sendMessage;
this.srcSendMessage = (type, payload) => {
- return pluginHost
+ return tb_plugin.host
.broadcast(type, payload)
.then(([result]) => result);
};
@@ -70,10 +68,10 @@ namespace tf_plugin.test {
spec: 'guest (src) to host (dest)',
beforeEachFunc: function() {
this.destWindow = window;
- this.destListen = pluginHost.listen;
- this.destUnlisten = pluginHost.unlisten;
+ this.destListen = tb_plugin.host.listen;
+ this.destUnlisten = tb_plugin.host.unlisten;
this.destSendMessage = (type, payload) => {
- return pluginHost
+ return tb_plugin.host
.broadcast(type, payload)
.then(([result]) => result);
};
@@ -220,4 +218,4 @@ namespace tf_plugin.test {
});
});
});
-} // namespace tf_plugin.test
+} // namespace tf_plugin.lib.DO_NOT_USE_INTERNAL
diff --git a/tensorboard/components/experimental/plugin_util/test/tests.html b/tensorboard/components/experimental/plugin_util/test/tests.html
index 76946a9b0e..20a4d13677 100644
--- a/tensorboard/components/experimental/plugin_util/test/tests.html
+++ b/tensorboard/components/experimental/plugin_util/test/tests.html
@@ -20,4 +20,4 @@
-
+
diff --git a/tensorboard/components/tf_tensorboard/BUILD b/tensorboard/components/tf_tensorboard/BUILD
index 4bffafc1a7..ec583c8122 100644
--- a/tensorboard/components/tf_tensorboard/BUILD
+++ b/tensorboard/components/tf_tensorboard/BUILD
@@ -23,6 +23,7 @@ tf_web_library(
"//tensorboard/components/tf_imports:polymer",
"//tensorboard/components/tf_paginated_view",
"//tensorboard/components/tf_storage",
+ "//tensorboard/components/experimental/plugin_util:plugin_host",
"@com_google_fonts_roboto",
"@org_polymer_iron_icons",
"@org_polymer_paper_button",
diff --git a/tensorboard/components/tf_tensorboard/tf-tensorboard.html b/tensorboard/components/tf_tensorboard/tf-tensorboard.html
index 36adac7827..a984ae7ec7 100644
--- a/tensorboard/components/tf_tensorboard/tf-tensorboard.html
+++ b/tensorboard/components/tf_tensorboard/tf-tensorboard.html
@@ -31,6 +31,7 @@
+