diff --git a/cmake/arrow/CMakeLists.txt b/cmake/arrow/CMakeLists.txt index 6712ad54c4..d9b9733bed 100644 --- a/cmake/arrow/CMakeLists.txt +++ b/cmake/arrow/CMakeLists.txt @@ -261,6 +261,9 @@ add_library(arrow STATIC ${ARROW_SRCS}) target_compile_definitions(arrow PUBLIC ARROW_NO_DEPRECATED_API) target_compile_definitions(arrow PUBLIC ARROW_STATIC) + +set(CMAKE_CXX_FLAGS_RELEASE " -pthread ") + # will need built boost filesystem and system .lib to work, even though # perspective itself does not use those dependencies target_link_libraries(arrow diff --git a/cmake/re2/CMakeLists.txt b/cmake/re2/CMakeLists.txt index 084c111da7..4050cad7cb 100644 --- a/cmake/re2/CMakeLists.txt +++ b/cmake/re2/CMakeLists.txt @@ -99,9 +99,7 @@ target_include_directories(re2 PUBLIC $ #include #include +#include #ifdef PSP_ENABLE_WASM #include diff --git a/cpp/perspective/src/include/perspective/first.h b/cpp/perspective/src/include/perspective/first.h index afc394799e..be0d43cb4e 100644 --- a/cpp/perspective/src/include/perspective/first.h +++ b/cpp/perspective/src/include/perspective/first.h @@ -7,11 +7,11 @@ * */ -#ifndef PSP_ENABLE_WASM +// #ifndef PSP_ENABLE_WASM #ifndef PSP_PARALLEL_FOR #define PSP_PARALLEL_FOR #endif -#endif +// #endif #if !defined(__linux__) && !defined(__APPLE__) && !defined(WIN32) // default to linux diff --git a/cpp/perspective/src/include/perspective/parallel_for.h b/cpp/perspective/src/include/perspective/parallel_for.h index 93ccd21214..c70de446c8 100644 --- a/cpp/perspective/src/include/perspective/parallel_for.h +++ b/cpp/perspective/src/include/perspective/parallel_for.h @@ -9,10 +9,8 @@ #pragma once -#ifdef PSP_ENABLE_PYTHON #include #include -#endif namespace perspective { diff --git a/packages/perspective/build.js b/packages/perspective/build.js index ada120a05c..989e0d01ee 100644 --- a/packages/perspective/build.js +++ b/packages/perspective/build.js @@ -6,6 +6,7 @@ const {WorkerPlugin} = require("@finos/perspective-build/worker"); const {NodeModulesExternal} = require("@finos/perspective-build/external"); const {UMDLoader} = require("@finos/perspective-build/umd"); const {build} = require("@finos/perspective-build/build"); +const {BlobPlugin} = require("@finos/perspective-build/blob"); const BUILD = [ { @@ -30,7 +31,7 @@ const BUILD = [ }, format: "esm", entryPoints: ["src/js/perspective.browser.js"], - plugins: [WasmPlugin(false), WorkerPlugin(false)], + plugins: [WasmPlugin(false), WorkerPlugin(false), BlobPlugin()], outfile: "dist/cdn/perspective.js", }, { diff --git a/packages/perspective/src/js/perspective.js b/packages/perspective/src/js/perspective.js index a4117a63d5..6db8be0fad 100644 --- a/packages/perspective/src/js/perspective.js +++ b/packages/perspective/src/js/perspective.js @@ -14,6 +14,7 @@ import {bindall, get_column_type} from "./utils.js"; import {Server} from "./api/server.js"; import formatters from "./view_formatters"; +import {promise} from "sinon"; // IE fix - chrono::steady_clock depends on performance.now() which does not // exist in IE workers @@ -35,7 +36,7 @@ const WARNED_KEYS = new Set(); * * @module perspective */ -export default function (Module) { +export default function (Module, module_url, worker_url) { let __MODULE__ = Module; let accessor = new DataAccessor(); const SIDES = ["zero", "one", "two"]; @@ -2258,17 +2259,39 @@ export default function (Module) { * @param {ArrayBuffer} buffer an ArrayBuffer or Buffer containing the * Perspective WASM code */ - init(msg) { + async init(msg) { if (typeof WebAssembly === "undefined") { throw new Error("WebAssembly not supported"); } else { - __MODULE__({ + const promise = new Promise((resolve) => { + // The easiest way to deal with emscripten's main thread + // completion, which has an awkward API. + self.__on_wasm_init__ = resolve; + }); + + // `__MODULE__` is async because we want both the source text + // (to pass to the worker threads) and the module (for this + // thread). + const {default: mod_constructor} = await __MODULE__; + const wasm_mod = await mod_constructor({ wasmBinary: msg.buffer, wasmJSMethod: "native-wasm", - }).then((mod) => { - __MODULE__ = mod; - super.init(msg); + mainScriptUrlOrBlob: module_url, + locateFile(file) { + // The `perspective.cpp.worker.js` file can't be + // correctly linked by esbuild/webpack due to because + // emscripten generated code outputs a dynamic string + // passed to `import()`, so we can only provide this + // file via `locateFile()`. + if (file.endsWith("worker.js")) { + return worker_url; + } + }, }); + + __MODULE__ = wasm_mod; + await promise; + super.init(msg); } } } diff --git a/packages/perspective/src/js/perspective.node.js b/packages/perspective/src/js/perspective.node.js index fee1c9d103..4f71ba4427 100644 --- a/packages/perspective/src/js/perspective.node.js +++ b/packages/perspective/src/js/perspective.node.js @@ -100,6 +100,8 @@ function perspective_assets(assets, host_psp) { response.setHeader("Access-Control-Request-Method", "*"); response.setHeader("Access-Control-Allow-Methods", "OPTIONS,GET"); response.setHeader("Access-Control-Allow-Headers", "*"); + response.setHeader(`Cross-Origin-Embedder-Policy`, `require-corp`); + response.setHeader(`Cross-Origin-Opener-Policy`, `same-origin`); let url = request.url.split(/[\?\#]/)[0]; diff --git a/packages/perspective/src/js/perspective.worker.js b/packages/perspective/src/js/perspective.worker.js index 0a39839567..ff969930b4 100644 --- a/packages/perspective/src/js/perspective.worker.js +++ b/packages/perspective/src/js/perspective.worker.js @@ -7,21 +7,43 @@ * */ -import load_perspective from "@finos/perspective/pkg/esm/perspective.cpp.js"; +import load_perspective_txt from "@finos/perspective/pkg/esm/perspective.cpp.js"; +import perspective_worker_txt from "@finos/perspective/pkg/esm/perspective.cpp.worker.js"; import perspective from "./perspective.js"; -let _perspective_instance; +const psp_url = (() => { + const blob = new Blob([load_perspective_txt], {type: "text/javascript"}); + return URL.createObjectURL(blob); +})(); + +const load_perspective = (async () => { + return await import(psp_url); +})(); + +const worker_url = (() => { + const worker_blob = new Blob([perspective_worker_txt], { + type: "text/javascript", + }); + return URL.createObjectURL(worker_blob); +})(); +let _perspective_instance; if (global.document !== undefined && typeof WebAssembly !== "undefined") { _perspective_instance = global.perspective = perspective( load_perspective({ wasmJSMethod: "native-wasm", printErr: (x) => console.error(x), print: (x) => console.log(x), - }) + }), + psp_url, + worker_url ); } else { - _perspective_instance = global.perspective = perspective(load_perspective); + _perspective_instance = global.perspective = perspective( + load_perspective, + psp_url, + worker_url + ); } export default _perspective_instance; diff --git a/rust/perspective-viewer/src/rust/components/filter_item.rs b/rust/perspective-viewer/src/rust/components/filter_item.rs index 06f267f9b1..73f06f775e 100644 --- a/rust/perspective-viewer/src/rust/components/filter_item.rs +++ b/rust/perspective-viewer/src/rust/components/filter_item.rs @@ -68,7 +68,9 @@ impl DragDropListItemProps for FilterItemProps { impl FilterItemProps { /// Does this filter item get a "suggestions" auto-complete modal? fn is_suggestable(&self) -> bool { - (self.filter.1 == FilterOp::EQ || self.filter.1 == FilterOp::NE || self.filter.1 == FilterOp::In) + (self.filter.1 == FilterOp::EQ + || self.filter.1 == FilterOp::NE + || self.filter.1 == FilterOp::In) && self.get_filter_type() == Some(Type::String) } @@ -259,7 +261,7 @@ impl Component for FilterItem { ctx.props().filter_dropdown.autocomplete( column, if ctx.props().filter.1 == FilterOp::In { - input.split(",").last().unwrap().to_owned() + input.split(',').last().unwrap().to_owned() } else { input.clone() }, diff --git a/rust/perspective-viewer/src/rust/model/structural.rs b/rust/perspective-viewer/src/rust/model/structural.rs index 04c4684996..ccbcc57d01 100644 --- a/rust/perspective-viewer/src/rust/model/structural.rs +++ b/rust/perspective-viewer/src/rust/model/structural.rs @@ -35,28 +35,28 @@ pub trait HasDragDrop { #[macro_export] macro_rules! derive_model { (DragDrop for $key:ty) => { - impl crate::model::HasDragDrop for $key { + impl $crate::model::HasDragDrop for $key { fn dragdrop(&self) -> &'_ DragDrop { &self.dragdrop } } }; (Renderer for $key:ty) => { - impl crate::model::HasRenderer for $key { + impl $crate::model::HasRenderer for $key { fn renderer(&self) -> &'_ Renderer { &self.renderer } } }; (Session for $key:ty) => { - impl crate::model::HasSession for $key { + impl $crate::model::HasSession for $key { fn session(&self) -> &'_ Session { &self.session } } }; (Theme for $key:ty) => { - impl crate::model::HasTheme for $key { + impl $crate::model::HasTheme for $key { fn theme(&self) -> &'_ Theme { &self.theme } diff --git a/rust/perspective-viewer/src/rust/utils/mod.rs b/rust/perspective-viewer/src/rust/utils/mod.rs index 79f2abc31d..29a8b2ce10 100644 --- a/rust/perspective-viewer/src/rust/utils/mod.rs +++ b/rust/perspective-viewer/src/rust/utils/mod.rs @@ -75,34 +75,62 @@ macro_rules! js_log_maybe { /// .. }` or `move async { .. }`, but for clone semantics. #[macro_export] macro_rules! clone { - ($i:ident) => { - let $i = $i.clone(); + (impl @bind $i:tt { $($orig:tt)* } { }) => { + let $i = $($orig)*.clone(); }; - ($i:ident, $($tt:tt)*) => { - clone!($i); - clone!($($tt)*); + + (impl @bind $i:tt { $($orig:tt)* } { $binder:tt }) => { + let $binder = $($orig)*.clone(); + }; + + (impl @expand { $($orig:tt)* } { $($binder:tt)* } $i:tt) => { + clone!(impl @bind $i { $($orig)* $i } { $($binder)* }); + }; + + (impl @expand { $($orig:tt)* } { $($binder:tt)* } $i:tt ()) => { + clone!(impl @bind $i { $($orig)* $i () } { $($binder)* }); + }; + + (impl @expand { $($orig:tt)* } { $($binder:tt)* } $i:tt . 0) => { + clone!(impl @bind $i { $($orig)* $i . 0 } { $($binder)* }); + }; + + (impl @expand { $($orig:tt)* } { $($binder:tt)* } $i:tt . 1) => { + clone!(impl @bind $i { $($orig)* $i . 1 } { $($binder)* }); }; - ($this:ident . $i:ident) => { - let $i = $this.$i.clone(); + + (impl @expand { $($orig:tt)* } { $($binder:tt)* } $i:tt . 2) => { + clone!(impl @bind $i { $($orig)* $i . 2 } { $($binder)* }); }; - ($this:ident . $i:ident, $($tt:tt)*) => { - clone!($this . $i); - clone!($($tt)*); + + (impl @expand { $($orig:tt)* } { $($binder:tt)* } $i:tt . 3) => { + clone!(impl @bind $i { $($orig)* $i . 3 } { $($binder)* }); }; - ($this:ident . $borrow:ident() . $i:ident) => { - let $i = $this.$borrow().$i.clone(); + + (impl @expand { $($orig:tt)* } { $($binder:tt)* } $i:tt = $($tail:tt)+) => { + clone!(impl @expand { $($orig)* } { $i } $($tail)+); }; - ($this:ident . $borrow:ident() . $i:ident, $($tt:tt)*) => { - clone!($this.$borrow().$i); - clone!($($tt)*); + + (impl @expand { $($orig:tt)* } { $($binder:tt)* } $i:tt $($tail:tt)+) => { + clone!(impl @expand { $($orig)* $i } { $($binder)* } $($tail)+); }; - ($this:ident . $borrow:ident()) => { - let $borrow = $this.$borrow().clone(); + + (impl @context { $($orig:tt)* } $tail:tt) => { + clone!(impl @expand { } { } $($orig)* $tail); }; - ($this:ident . $borrow:ident(), $($tt:tt)*) => { - clone!($this.$borrow()); - clone!($($tt)*); + + (impl @context { $($orig:tt)* } , $($tail:tt)+) => { + clone!(impl @expand { } { } $($orig)*); + clone!(impl @context { } $($tail)+); + }; + + (impl @context { $($orig:tt)* } $i:tt $($tail:tt)+) => { + clone!(impl @context { $($orig)* $i } $($tail)+); }; + + ($($tail:tt)+) => { + clone!(impl @context { } $($tail)+); + } } #[macro_export] diff --git a/tools/perspective-build/blob.js b/tools/perspective-build/blob.js new file mode 100644 index 0000000000..390728ad9c --- /dev/null +++ b/tools/perspective-build/blob.js @@ -0,0 +1,30 @@ +const fs = require("fs"); + +exports.BlobPlugin = function BlobPlugin() { + function setup(build) { + build.onResolve( + {filter: /perspective\.cpp(\.worker)?\.js$/}, + (args) => { + return { + path: args.path, + namespace: "blob-inline", + }; + } + ); + + build.onLoad({filter: /.*/, namespace: "blob-inline"}, async (args) => { + const path = require.resolve(args.path); + const contents = await fs.promises.readFile(path); + return { + contents: ` + export default ${JSON.stringify(contents.toString())}; + `, + }; + }); + } + + return { + name: "blob", + setup, + }; +}; diff --git a/tools/perspective-build/worker.js b/tools/perspective-build/worker.js index 9782433dc9..d9346d337f 100644 --- a/tools/perspective-build/worker.js +++ b/tools/perspective-build/worker.js @@ -1,6 +1,7 @@ const fs = require("fs"); const path = require("path"); const esbuild = require("esbuild"); +const {BlobPlugin} = require("./blob.js"); exports.WorkerPlugin = function WorkerPlugin(inline) { function setup(build) { @@ -20,6 +21,7 @@ exports.WorkerPlugin = function WorkerPlugin(inline) { minify: !process.env.PSP_DEBUG, bundle: true, sourcemap: false, + plugins: [BlobPlugin()], }); return {