From 5fce565af705ede3a39ea3ff258442433432df57 Mon Sep 17 00:00:00 2001 From: Trevor Manz Date: Tue, 30 Jul 2024 21:55:50 -0400 Subject: [PATCH 1/5] Ensure binary data is a DataView --- js/src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/src/utils.ts b/js/src/utils.ts index b69d55f..697f849 100644 --- a/js/src/utils.ts +++ b/js/src/utils.ts @@ -5,7 +5,7 @@ import { decode } from 'base64-arraybuffer'; // along to the comm logic function jsonParse(x: string) { const msg = JSON.parse(x); - msg.buffers = msg.buffers.map((b: any) => decode(b)); + msg.buffers = msg.buffers.map((b: any) => new DataView(decode(b))); return msg; } From 6d9a3a533493ab37ee80ef5b70948d6c62c02075 Mon Sep 17 00:00:00 2001 From: Trevor Manz Date: Tue, 30 Jul 2024 22:13:31 -0400 Subject: [PATCH 2/5] Make explicit --- js/src/comm.ts | 1 + js/src/utils.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/js/src/comm.ts b/js/src/comm.ts index 948b773..3c1c7b0 100644 --- a/js/src/comm.ts +++ b/js/src/comm.ts @@ -32,6 +32,7 @@ export class ShinyComm { const msg = { content: {comm_id: this.comm_id, data: data}, metadata: metadata, + // TODO: need to _encode_ any buffers into base64 (JSON.stringify just drops them) buffers: buffers || [], // this doesn't seem relevant to the widget? header: {} diff --git a/js/src/utils.ts b/js/src/utils.ts index 697f849..e02ebaf 100644 --- a/js/src/utils.ts +++ b/js/src/utils.ts @@ -5,7 +5,7 @@ import { decode } from 'base64-arraybuffer'; // along to the comm logic function jsonParse(x: string) { const msg = JSON.parse(x); - msg.buffers = msg.buffers.map((b: any) => new DataView(decode(b))); + msg.buffers = msg.buffers.map((base64: string) => new DataView(decode(base64))); return msg; } From 30aedcff8b08d76147fb288060a196b3986aaffa Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 31 Jul 2024 10:27:44 -0500 Subject: [PATCH 3/5] Encode buffers into base64 before serializing & sending from client to server --- js/src/comm.ts | 11 +++++++++-- shinywidgets/static/output.js | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/js/src/comm.ts b/js/src/comm.ts index 3c1c7b0..28a03d8 100644 --- a/js/src/comm.ts +++ b/js/src/comm.ts @@ -1,3 +1,4 @@ +import { encode } from "base64-arraybuffer"; import { Throttler } from "./utils"; // This class is a striped down version of Comm from @jupyter-widgets/base @@ -29,11 +30,17 @@ export class ShinyComm { metadata?: any, buffers?: ArrayBuffer[] | ArrayBufferView[] ): string { + + // Encode buffers as base64 before stringifying the message + const buffers_64 = (buffers || []).map((b: ArrayBuffer | ArrayBufferView) => { + const buffer = b instanceof ArrayBuffer ? b : b.buffer; + return encode(buffer); + }); + const msg = { content: {comm_id: this.comm_id, data: data}, metadata: metadata, - // TODO: need to _encode_ any buffers into base64 (JSON.stringify just drops them) - buffers: buffers || [], + buffers: buffers_64, // this doesn't seem relevant to the widget? header: {} }; diff --git a/shinywidgets/static/output.js b/shinywidgets/static/output.js index fc2289a..682599a 100644 --- a/shinywidgets/static/output.js +++ b/shinywidgets/static/output.js @@ -26,7 +26,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac \*********************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"ShinyComm\": () => (/* binding */ ShinyComm)\n/* harmony export */ });\n/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils */ \"./src/utils.ts\");\n\n// This class is a striped down version of Comm from @jupyter-widgets/base\n// https://github.com/jupyter-widgets/ipywidgets/blob/88cec8/packages/base/src/services-shim.ts#L192-L335\n// Note that the Kernel.IComm implementation is located here\n// https://github.com/jupyterlab/jupyterlab/blob/master/packages/services/src/kernel/comm.ts\nclass ShinyComm {\n constructor(model_id) {\n this.comm_id = model_id;\n // TODO: make this configurable (see comments in send() below)?\n this.throttler = new _utils__WEBPACK_IMPORTED_MODULE_0__.Throttler(100);\n }\n // This might not be needed\n get target_name() {\n return \"jupyter.widgets\";\n }\n send(data, callbacks, metadata, buffers) {\n const msg = {\n content: { comm_id: this.comm_id, data: data },\n metadata: metadata,\n buffers: buffers || [],\n // this doesn't seem relevant to the widget?\n header: {}\n };\n const msg_txt = JSON.stringify(msg);\n // Since ipyleaflet can send mousemove events very quickly when hovering over the map,\n // we throttle them to ensure that the server doesn't get overwhelmed. Said events\n // generate a payload that looks like this:\n // {\"method\": \"custom\", \"content\": {\"event\": \"interaction\", \"type\": \"mousemove\", \"coordinates\": [-17.76259815404015, 12.096729340756617]}}\n //\n // TODO: This is definitely not ideal. It would be better to have a way to specify/\n // customize throttle rates instead of having such a targetted fix for ipyleaflet.\n const is_mousemove = data.method === \"custom\" &&\n data.content.event === \"interaction\" &&\n data.content.type === \"mousemove\";\n if (is_mousemove) {\n this.throttler.throttle(() => {\n Shiny.setInputValue(\"shinywidgets_comm_send\", msg_txt, { priority: \"event\" });\n });\n }\n else {\n this.throttler.flush();\n Shiny.setInputValue(\"shinywidgets_comm_send\", msg_txt, { priority: \"event\" });\n }\n // When client-side changes happen to the WidgetModel, this send method\n // won't get called for _every_ change (just the first one). The\n // expectation is that this method will eventually end up calling itself\n // (via callbacks) when the server is ready (i.e., idle) to receive more\n // updates. To make sense of this, see\n // https://github.com/jupyter-widgets/ipywidgets/blob/88cec8b/packages/base/src/widget.ts#L550-L557\n if (callbacks && callbacks.iopub && callbacks.iopub.status) {\n setTimeout(() => {\n // TODO-future: it doesn't seem quite right to report that shiny is always idle.\n // Maybe listen to the shiny-busy flag?\n // const state = document.querySelector(\"html\").classList.contains(\"shiny-busy\") ? \"busy\" : \"idle\";\n const msg = { content: { execution_state: \"idle\" } };\n callbacks.iopub.status(msg);\n }, 0);\n }\n return this.comm_id;\n }\n open(data, callbacks, metadata, buffers) {\n // I don't think we need to do anything here?\n return this.comm_id;\n }\n close(data, callbacks, metadata, buffers) {\n // I don't think we need to do anything here?\n return this.comm_id;\n }\n on_msg(callback) {\n this._msg_callback = callback.bind(this);\n }\n on_close(callback) {\n this._close_callback = callback.bind(this);\n }\n handle_msg(msg) {\n if (this._msg_callback)\n this._msg_callback(msg);\n }\n handle_close(msg) {\n if (this._close_callback)\n this._close_callback(msg);\n }\n}\n\n\n//# sourceURL=webpack://@jupyter-widgets/shiny-embed-manager/./src/comm.ts?"); +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"ShinyComm\": () => (/* binding */ ShinyComm)\n/* harmony export */ });\n/* harmony import */ var base64_arraybuffer__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! base64-arraybuffer */ \"./node_modules/base64-arraybuffer/dist/base64-arraybuffer.es5.js\");\n/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./utils */ \"./src/utils.ts\");\n\n\n// This class is a striped down version of Comm from @jupyter-widgets/base\n// https://github.com/jupyter-widgets/ipywidgets/blob/88cec8/packages/base/src/services-shim.ts#L192-L335\n// Note that the Kernel.IComm implementation is located here\n// https://github.com/jupyterlab/jupyterlab/blob/master/packages/services/src/kernel/comm.ts\nclass ShinyComm {\n constructor(model_id) {\n this.comm_id = model_id;\n // TODO: make this configurable (see comments in send() below)?\n this.throttler = new _utils__WEBPACK_IMPORTED_MODULE_1__.Throttler(100);\n }\n // This might not be needed\n get target_name() {\n return \"jupyter.widgets\";\n }\n send(data, callbacks, metadata, buffers) {\n // Encode buffers as base64 before stringifying the message\n const buffers_64 = (buffers || []).map((b) => {\n const buffer = b instanceof ArrayBuffer ? b : b.buffer;\n return (0,base64_arraybuffer__WEBPACK_IMPORTED_MODULE_0__.encode)(buffer);\n });\n const msg = {\n content: { comm_id: this.comm_id, data: data },\n metadata: metadata,\n buffers: buffers_64,\n // this doesn't seem relevant to the widget?\n header: {}\n };\n const msg_txt = JSON.stringify(msg);\n // Since ipyleaflet can send mousemove events very quickly when hovering over the map,\n // we throttle them to ensure that the server doesn't get overwhelmed. Said events\n // generate a payload that looks like this:\n // {\"method\": \"custom\", \"content\": {\"event\": \"interaction\", \"type\": \"mousemove\", \"coordinates\": [-17.76259815404015, 12.096729340756617]}}\n //\n // TODO: This is definitely not ideal. It would be better to have a way to specify/\n // customize throttle rates instead of having such a targetted fix for ipyleaflet.\n const is_mousemove = data.method === \"custom\" &&\n data.content.event === \"interaction\" &&\n data.content.type === \"mousemove\";\n if (is_mousemove) {\n this.throttler.throttle(() => {\n Shiny.setInputValue(\"shinywidgets_comm_send\", msg_txt, { priority: \"event\" });\n });\n }\n else {\n this.throttler.flush();\n Shiny.setInputValue(\"shinywidgets_comm_send\", msg_txt, { priority: \"event\" });\n }\n // When client-side changes happen to the WidgetModel, this send method\n // won't get called for _every_ change (just the first one). The\n // expectation is that this method will eventually end up calling itself\n // (via callbacks) when the server is ready (i.e., idle) to receive more\n // updates. To make sense of this, see\n // https://github.com/jupyter-widgets/ipywidgets/blob/88cec8b/packages/base/src/widget.ts#L550-L557\n if (callbacks && callbacks.iopub && callbacks.iopub.status) {\n setTimeout(() => {\n // TODO-future: it doesn't seem quite right to report that shiny is always idle.\n // Maybe listen to the shiny-busy flag?\n // const state = document.querySelector(\"html\").classList.contains(\"shiny-busy\") ? \"busy\" : \"idle\";\n const msg = { content: { execution_state: \"idle\" } };\n callbacks.iopub.status(msg);\n }, 0);\n }\n return this.comm_id;\n }\n open(data, callbacks, metadata, buffers) {\n // I don't think we need to do anything here?\n return this.comm_id;\n }\n close(data, callbacks, metadata, buffers) {\n // I don't think we need to do anything here?\n return this.comm_id;\n }\n on_msg(callback) {\n this._msg_callback = callback.bind(this);\n }\n on_close(callback) {\n this._close_callback = callback.bind(this);\n }\n handle_msg(msg) {\n if (this._msg_callback)\n this._msg_callback(msg);\n }\n handle_close(msg) {\n if (this._close_callback)\n this._close_callback(msg);\n }\n}\n\n\n//# sourceURL=webpack://@jupyter-widgets/shiny-embed-manager/./src/comm.ts?"); /***/ }), @@ -46,7 +46,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _jup \**********************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"Throttler\": () => (/* binding */ Throttler),\n/* harmony export */ \"jsonParse\": () => (/* binding */ jsonParse)\n/* harmony export */ });\n/* harmony import */ var base64_arraybuffer__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! base64-arraybuffer */ \"./node_modules/base64-arraybuffer/dist/base64-arraybuffer.es5.js\");\n\n// On the server, we're using jupyter_client.session.json_packer to serialize messages,\n// and it encodes binary data (i.e., buffers) as base64, so decode it before passing it\n// along to the comm logic\nfunction jsonParse(x) {\n const msg = JSON.parse(x);\n msg.buffers = msg.buffers.map((b) => (0,base64_arraybuffer__WEBPACK_IMPORTED_MODULE_0__.decode)(b));\n return msg;\n}\nclass Throttler {\n constructor(wait = 100) {\n if (wait < 0)\n throw new Error(\"wait must be a positive number\");\n this.wait = wait;\n this._reset();\n }\n // Try to execute the function immediately, if it is not waiting\n // If it is waiting, update the function to be called\n throttle(fn) {\n if (fn.length > 0)\n throw new Error(\"fn must not take any arguments\");\n if (this.isWaiting) {\n // If the timeout is currently waiting, update the func to be called\n this.fnToCall = fn;\n }\n else {\n // If there is nothing waiting, call it immediately\n // and start the throttling\n fn();\n this._setTimeout();\n }\n }\n // Execute the function immediately and reset the timeout\n // This is useful when the timeout is waiting and we want to\n // execute the function immediately to not have events be out\n // of order\n flush() {\n if (this.fnToCall)\n this.fnToCall();\n this._reset();\n }\n _setTimeout() {\n this.timeoutId = setTimeout(() => {\n if (this.fnToCall) {\n this.fnToCall();\n this.fnToCall = null;\n // Restart the timeout as we just called the function\n // This call is the key step of Throttler\n this._setTimeout();\n }\n else {\n this._reset();\n }\n }, this.wait);\n }\n _reset() {\n this.fnToCall = null;\n clearTimeout(this.timeoutId);\n this.timeoutId = null;\n }\n get isWaiting() {\n return this.timeoutId !== null;\n }\n}\n\n\n\n//# sourceURL=webpack://@jupyter-widgets/shiny-embed-manager/./src/utils.ts?"); +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"Throttler\": () => (/* binding */ Throttler),\n/* harmony export */ \"jsonParse\": () => (/* binding */ jsonParse)\n/* harmony export */ });\n/* harmony import */ var base64_arraybuffer__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! base64-arraybuffer */ \"./node_modules/base64-arraybuffer/dist/base64-arraybuffer.es5.js\");\n\n// On the server, we're using jupyter_client.session.json_packer to serialize messages,\n// and it encodes binary data (i.e., buffers) as base64, so decode it before passing it\n// along to the comm logic\nfunction jsonParse(x) {\n const msg = JSON.parse(x);\n msg.buffers = msg.buffers.map((base64) => new DataView((0,base64_arraybuffer__WEBPACK_IMPORTED_MODULE_0__.decode)(base64)));\n return msg;\n}\nclass Throttler {\n constructor(wait = 100) {\n if (wait < 0)\n throw new Error(\"wait must be a positive number\");\n this.wait = wait;\n this._reset();\n }\n // Try to execute the function immediately, if it is not waiting\n // If it is waiting, update the function to be called\n throttle(fn) {\n if (fn.length > 0)\n throw new Error(\"fn must not take any arguments\");\n if (this.isWaiting) {\n // If the timeout is currently waiting, update the func to be called\n this.fnToCall = fn;\n }\n else {\n // If there is nothing waiting, call it immediately\n // and start the throttling\n fn();\n this._setTimeout();\n }\n }\n // Execute the function immediately and reset the timeout\n // This is useful when the timeout is waiting and we want to\n // execute the function immediately to not have events be out\n // of order\n flush() {\n if (this.fnToCall)\n this.fnToCall();\n this._reset();\n }\n _setTimeout() {\n this.timeoutId = setTimeout(() => {\n if (this.fnToCall) {\n this.fnToCall();\n this.fnToCall = null;\n // Restart the timeout as we just called the function\n // This call is the key step of Throttler\n this._setTimeout();\n }\n else {\n this._reset();\n }\n }, this.wait);\n }\n _reset() {\n this.fnToCall = null;\n clearTimeout(this.timeoutId);\n this.timeoutId = null;\n }\n get isWaiting() {\n return this.timeoutId !== null;\n }\n}\n\n\n\n//# sourceURL=webpack://@jupyter-widgets/shiny-embed-manager/./src/utils.ts?"); /***/ }), From 0b5e81a7365905c7cdc9ac9e549a80c8c94a9bc6 Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 31 Jul 2024 16:14:54 -0500 Subject: [PATCH 4/5] Revert "Encode buffers into base64 before serializing & sending from client to server" This reverts commit 30aedcff8b08d76147fb288060a196b3986aaffa. --- js/src/comm.ts | 11 ++--------- shinywidgets/static/output.js | 4 ++-- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/js/src/comm.ts b/js/src/comm.ts index 28a03d8..3c1c7b0 100644 --- a/js/src/comm.ts +++ b/js/src/comm.ts @@ -1,4 +1,3 @@ -import { encode } from "base64-arraybuffer"; import { Throttler } from "./utils"; // This class is a striped down version of Comm from @jupyter-widgets/base @@ -30,17 +29,11 @@ export class ShinyComm { metadata?: any, buffers?: ArrayBuffer[] | ArrayBufferView[] ): string { - - // Encode buffers as base64 before stringifying the message - const buffers_64 = (buffers || []).map((b: ArrayBuffer | ArrayBufferView) => { - const buffer = b instanceof ArrayBuffer ? b : b.buffer; - return encode(buffer); - }); - const msg = { content: {comm_id: this.comm_id, data: data}, metadata: metadata, - buffers: buffers_64, + // TODO: need to _encode_ any buffers into base64 (JSON.stringify just drops them) + buffers: buffers || [], // this doesn't seem relevant to the widget? header: {} }; diff --git a/shinywidgets/static/output.js b/shinywidgets/static/output.js index 682599a..fc2289a 100644 --- a/shinywidgets/static/output.js +++ b/shinywidgets/static/output.js @@ -26,7 +26,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac \*********************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"ShinyComm\": () => (/* binding */ ShinyComm)\n/* harmony export */ });\n/* harmony import */ var base64_arraybuffer__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! base64-arraybuffer */ \"./node_modules/base64-arraybuffer/dist/base64-arraybuffer.es5.js\");\n/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./utils */ \"./src/utils.ts\");\n\n\n// This class is a striped down version of Comm from @jupyter-widgets/base\n// https://github.com/jupyter-widgets/ipywidgets/blob/88cec8/packages/base/src/services-shim.ts#L192-L335\n// Note that the Kernel.IComm implementation is located here\n// https://github.com/jupyterlab/jupyterlab/blob/master/packages/services/src/kernel/comm.ts\nclass ShinyComm {\n constructor(model_id) {\n this.comm_id = model_id;\n // TODO: make this configurable (see comments in send() below)?\n this.throttler = new _utils__WEBPACK_IMPORTED_MODULE_1__.Throttler(100);\n }\n // This might not be needed\n get target_name() {\n return \"jupyter.widgets\";\n }\n send(data, callbacks, metadata, buffers) {\n // Encode buffers as base64 before stringifying the message\n const buffers_64 = (buffers || []).map((b) => {\n const buffer = b instanceof ArrayBuffer ? b : b.buffer;\n return (0,base64_arraybuffer__WEBPACK_IMPORTED_MODULE_0__.encode)(buffer);\n });\n const msg = {\n content: { comm_id: this.comm_id, data: data },\n metadata: metadata,\n buffers: buffers_64,\n // this doesn't seem relevant to the widget?\n header: {}\n };\n const msg_txt = JSON.stringify(msg);\n // Since ipyleaflet can send mousemove events very quickly when hovering over the map,\n // we throttle them to ensure that the server doesn't get overwhelmed. Said events\n // generate a payload that looks like this:\n // {\"method\": \"custom\", \"content\": {\"event\": \"interaction\", \"type\": \"mousemove\", \"coordinates\": [-17.76259815404015, 12.096729340756617]}}\n //\n // TODO: This is definitely not ideal. It would be better to have a way to specify/\n // customize throttle rates instead of having such a targetted fix for ipyleaflet.\n const is_mousemove = data.method === \"custom\" &&\n data.content.event === \"interaction\" &&\n data.content.type === \"mousemove\";\n if (is_mousemove) {\n this.throttler.throttle(() => {\n Shiny.setInputValue(\"shinywidgets_comm_send\", msg_txt, { priority: \"event\" });\n });\n }\n else {\n this.throttler.flush();\n Shiny.setInputValue(\"shinywidgets_comm_send\", msg_txt, { priority: \"event\" });\n }\n // When client-side changes happen to the WidgetModel, this send method\n // won't get called for _every_ change (just the first one). The\n // expectation is that this method will eventually end up calling itself\n // (via callbacks) when the server is ready (i.e., idle) to receive more\n // updates. To make sense of this, see\n // https://github.com/jupyter-widgets/ipywidgets/blob/88cec8b/packages/base/src/widget.ts#L550-L557\n if (callbacks && callbacks.iopub && callbacks.iopub.status) {\n setTimeout(() => {\n // TODO-future: it doesn't seem quite right to report that shiny is always idle.\n // Maybe listen to the shiny-busy flag?\n // const state = document.querySelector(\"html\").classList.contains(\"shiny-busy\") ? \"busy\" : \"idle\";\n const msg = { content: { execution_state: \"idle\" } };\n callbacks.iopub.status(msg);\n }, 0);\n }\n return this.comm_id;\n }\n open(data, callbacks, metadata, buffers) {\n // I don't think we need to do anything here?\n return this.comm_id;\n }\n close(data, callbacks, metadata, buffers) {\n // I don't think we need to do anything here?\n return this.comm_id;\n }\n on_msg(callback) {\n this._msg_callback = callback.bind(this);\n }\n on_close(callback) {\n this._close_callback = callback.bind(this);\n }\n handle_msg(msg) {\n if (this._msg_callback)\n this._msg_callback(msg);\n }\n handle_close(msg) {\n if (this._close_callback)\n this._close_callback(msg);\n }\n}\n\n\n//# sourceURL=webpack://@jupyter-widgets/shiny-embed-manager/./src/comm.ts?"); +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"ShinyComm\": () => (/* binding */ ShinyComm)\n/* harmony export */ });\n/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils */ \"./src/utils.ts\");\n\n// This class is a striped down version of Comm from @jupyter-widgets/base\n// https://github.com/jupyter-widgets/ipywidgets/blob/88cec8/packages/base/src/services-shim.ts#L192-L335\n// Note that the Kernel.IComm implementation is located here\n// https://github.com/jupyterlab/jupyterlab/blob/master/packages/services/src/kernel/comm.ts\nclass ShinyComm {\n constructor(model_id) {\n this.comm_id = model_id;\n // TODO: make this configurable (see comments in send() below)?\n this.throttler = new _utils__WEBPACK_IMPORTED_MODULE_0__.Throttler(100);\n }\n // This might not be needed\n get target_name() {\n return \"jupyter.widgets\";\n }\n send(data, callbacks, metadata, buffers) {\n const msg = {\n content: { comm_id: this.comm_id, data: data },\n metadata: metadata,\n buffers: buffers || [],\n // this doesn't seem relevant to the widget?\n header: {}\n };\n const msg_txt = JSON.stringify(msg);\n // Since ipyleaflet can send mousemove events very quickly when hovering over the map,\n // we throttle them to ensure that the server doesn't get overwhelmed. Said events\n // generate a payload that looks like this:\n // {\"method\": \"custom\", \"content\": {\"event\": \"interaction\", \"type\": \"mousemove\", \"coordinates\": [-17.76259815404015, 12.096729340756617]}}\n //\n // TODO: This is definitely not ideal. It would be better to have a way to specify/\n // customize throttle rates instead of having such a targetted fix for ipyleaflet.\n const is_mousemove = data.method === \"custom\" &&\n data.content.event === \"interaction\" &&\n data.content.type === \"mousemove\";\n if (is_mousemove) {\n this.throttler.throttle(() => {\n Shiny.setInputValue(\"shinywidgets_comm_send\", msg_txt, { priority: \"event\" });\n });\n }\n else {\n this.throttler.flush();\n Shiny.setInputValue(\"shinywidgets_comm_send\", msg_txt, { priority: \"event\" });\n }\n // When client-side changes happen to the WidgetModel, this send method\n // won't get called for _every_ change (just the first one). The\n // expectation is that this method will eventually end up calling itself\n // (via callbacks) when the server is ready (i.e., idle) to receive more\n // updates. To make sense of this, see\n // https://github.com/jupyter-widgets/ipywidgets/blob/88cec8b/packages/base/src/widget.ts#L550-L557\n if (callbacks && callbacks.iopub && callbacks.iopub.status) {\n setTimeout(() => {\n // TODO-future: it doesn't seem quite right to report that shiny is always idle.\n // Maybe listen to the shiny-busy flag?\n // const state = document.querySelector(\"html\").classList.contains(\"shiny-busy\") ? \"busy\" : \"idle\";\n const msg = { content: { execution_state: \"idle\" } };\n callbacks.iopub.status(msg);\n }, 0);\n }\n return this.comm_id;\n }\n open(data, callbacks, metadata, buffers) {\n // I don't think we need to do anything here?\n return this.comm_id;\n }\n close(data, callbacks, metadata, buffers) {\n // I don't think we need to do anything here?\n return this.comm_id;\n }\n on_msg(callback) {\n this._msg_callback = callback.bind(this);\n }\n on_close(callback) {\n this._close_callback = callback.bind(this);\n }\n handle_msg(msg) {\n if (this._msg_callback)\n this._msg_callback(msg);\n }\n handle_close(msg) {\n if (this._close_callback)\n this._close_callback(msg);\n }\n}\n\n\n//# sourceURL=webpack://@jupyter-widgets/shiny-embed-manager/./src/comm.ts?"); /***/ }), @@ -46,7 +46,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _jup \**********************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"Throttler\": () => (/* binding */ Throttler),\n/* harmony export */ \"jsonParse\": () => (/* binding */ jsonParse)\n/* harmony export */ });\n/* harmony import */ var base64_arraybuffer__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! base64-arraybuffer */ \"./node_modules/base64-arraybuffer/dist/base64-arraybuffer.es5.js\");\n\n// On the server, we're using jupyter_client.session.json_packer to serialize messages,\n// and it encodes binary data (i.e., buffers) as base64, so decode it before passing it\n// along to the comm logic\nfunction jsonParse(x) {\n const msg = JSON.parse(x);\n msg.buffers = msg.buffers.map((base64) => new DataView((0,base64_arraybuffer__WEBPACK_IMPORTED_MODULE_0__.decode)(base64)));\n return msg;\n}\nclass Throttler {\n constructor(wait = 100) {\n if (wait < 0)\n throw new Error(\"wait must be a positive number\");\n this.wait = wait;\n this._reset();\n }\n // Try to execute the function immediately, if it is not waiting\n // If it is waiting, update the function to be called\n throttle(fn) {\n if (fn.length > 0)\n throw new Error(\"fn must not take any arguments\");\n if (this.isWaiting) {\n // If the timeout is currently waiting, update the func to be called\n this.fnToCall = fn;\n }\n else {\n // If there is nothing waiting, call it immediately\n // and start the throttling\n fn();\n this._setTimeout();\n }\n }\n // Execute the function immediately and reset the timeout\n // This is useful when the timeout is waiting and we want to\n // execute the function immediately to not have events be out\n // of order\n flush() {\n if (this.fnToCall)\n this.fnToCall();\n this._reset();\n }\n _setTimeout() {\n this.timeoutId = setTimeout(() => {\n if (this.fnToCall) {\n this.fnToCall();\n this.fnToCall = null;\n // Restart the timeout as we just called the function\n // This call is the key step of Throttler\n this._setTimeout();\n }\n else {\n this._reset();\n }\n }, this.wait);\n }\n _reset() {\n this.fnToCall = null;\n clearTimeout(this.timeoutId);\n this.timeoutId = null;\n }\n get isWaiting() {\n return this.timeoutId !== null;\n }\n}\n\n\n\n//# sourceURL=webpack://@jupyter-widgets/shiny-embed-manager/./src/utils.ts?"); +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"Throttler\": () => (/* binding */ Throttler),\n/* harmony export */ \"jsonParse\": () => (/* binding */ jsonParse)\n/* harmony export */ });\n/* harmony import */ var base64_arraybuffer__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! base64-arraybuffer */ \"./node_modules/base64-arraybuffer/dist/base64-arraybuffer.es5.js\");\n\n// On the server, we're using jupyter_client.session.json_packer to serialize messages,\n// and it encodes binary data (i.e., buffers) as base64, so decode it before passing it\n// along to the comm logic\nfunction jsonParse(x) {\n const msg = JSON.parse(x);\n msg.buffers = msg.buffers.map((b) => (0,base64_arraybuffer__WEBPACK_IMPORTED_MODULE_0__.decode)(b));\n return msg;\n}\nclass Throttler {\n constructor(wait = 100) {\n if (wait < 0)\n throw new Error(\"wait must be a positive number\");\n this.wait = wait;\n this._reset();\n }\n // Try to execute the function immediately, if it is not waiting\n // If it is waiting, update the function to be called\n throttle(fn) {\n if (fn.length > 0)\n throw new Error(\"fn must not take any arguments\");\n if (this.isWaiting) {\n // If the timeout is currently waiting, update the func to be called\n this.fnToCall = fn;\n }\n else {\n // If there is nothing waiting, call it immediately\n // and start the throttling\n fn();\n this._setTimeout();\n }\n }\n // Execute the function immediately and reset the timeout\n // This is useful when the timeout is waiting and we want to\n // execute the function immediately to not have events be out\n // of order\n flush() {\n if (this.fnToCall)\n this.fnToCall();\n this._reset();\n }\n _setTimeout() {\n this.timeoutId = setTimeout(() => {\n if (this.fnToCall) {\n this.fnToCall();\n this.fnToCall = null;\n // Restart the timeout as we just called the function\n // This call is the key step of Throttler\n this._setTimeout();\n }\n else {\n this._reset();\n }\n }, this.wait);\n }\n _reset() {\n this.fnToCall = null;\n clearTimeout(this.timeoutId);\n this.timeoutId = null;\n }\n get isWaiting() {\n return this.timeoutId !== null;\n }\n}\n\n\n\n//# sourceURL=webpack://@jupyter-widgets/shiny-embed-manager/./src/utils.ts?"); /***/ }), From c987cd2c2a2868184d9ec3e15f3e34e194f1aae6 Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 31 Jul 2024 16:20:49 -0500 Subject: [PATCH 5/5] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1058cef..72c99ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [UNRELEASED] - +* Fixed a bug with receiving binary data on the frontend, which gets [quak](https://github.com/manzt/quak) and [mosaic-widget](https://idl.uw.edu/mosaic/jupyter/) working with `@render_widget`. (#152) ## [0.3.2] - 2024-04-16