How do I load a WASM module in a Vue component without initializing the module every time? - ecmascript-6

I have created a Rust library of type cdylib using
cargo web build --target=wasm32-unknown-unknown
I use a modified version of the "rust-wasm-loader" NPM package to build and load the WASM file. rust-wasm-loader uses this as a way to use the Rust code:
const wasm = require('./main.rs')
wasm.initialize().then(module => {
// Use your module here
const doub = module.cwrap('doub', 'number', ['number'])
console.log(doub(21))
})
I do not want to initialize the module every time I want to use the code. How do I load the module and use it like a library?

Since the loading of WebAssembly is asynchronous and may actually take some time for large modules, you need to handle the state when the module is not loaded, and then let the rest of the application know when the WebAssembly module is loaded.
You do not say how you are handling state in your Vue application, but if you are e.g. using Vuex you can do something like this:
const doubPlugin = store => {
wasm.initialize().then(module => {
const doub = module.cwrap('doub', 'number', ['number'])
store.subscribe((mutation, state) => {
if (mutation.type === 'DOUB_REQUEST') {
store.commit('DOUB_RESULT', doub(mutation.payload))
}
})
store.commit('DOUB_READY')
})
}
const store = new Vuex.Store({
state,
mutations,
plugins: [doubPlugin]
})
I've done a similar thing in an Elm/WebAssembly application (relevant JavaScript), so if you want to see how this can be applied in practice you can check that out.

Making a wrapper JS module that performs initialization and re-exports the promise seems like the most straightforward approach.
// main.js
module.exports = require("./main.rs").initialize().then(module => {
return {
doub: module.cwrap('doub', 'number', ['number'])
};
});
Then anything can do
require("./main.js").then(api => {
console.log(api.doub(21));
});
and will always get the same module. Or alternatively you could invert the async part and do
// main.js
const api = require("./main.rs").initialize().then(module => {
return {
doub: module.cwrap('doub', 'number', ['number'])
};
});
exports.doub = async function (val) {
return (await api).doub(val);
};
Then users of your module could do
const api = require("./main.js");
api.doub(21).then(result => {
console.log(result);
});

I created a class to wrap the WebAssembly loading and created a cwrap for every function:
class mkLib {
ready = false
_mod = require("./main.rs").initialize().then(module => {
this._mod = module
this.doub = module.cwrap('doub', 'number', ['number'])
this.ready = true
})
}
export default mkLib
In the Vue component's data there is a variable for the new class and in watch I wait for a change in the ready property:
data () {
return {
mod: new mkLib,
ready: false
}
},
watch: {
'mod.ready': function () {
this.ready = true
// now this.mod.FUNC(PARAMS) can be used
console.log(this.mod.doub(20))
}
}

Related

How to mock Forge Viewer in React Unit Tests

we're currently trying to unit / integration test our react application, which uses the forge viewer cdn script.
the to be tested code assumes that Autodesk is available on the window object, which is the case in the application (via script tags), but not in the context of testing. this leads to errors like these:
Test suite failed to run
ReferenceError: Autodesk is not defined
> 1 | export class ExtendedGuiViewer3D extends Autodesk.Viewing.GuiViewer3D {
according to the license comments, the forge viewer script only allows using it through the Autodesk servers, so I cant just download it, and require the file locally.
has anyone successfully tested components that use the forge viewer scripts?
Intro
Disclaimer, I've only recently been experimenting with "Reactifying" the Autodesk Forge Viewer!
I currently believe the 'correct' way to consume the forge viewer css / js is to pull code from the Autodesk hosted cdn. The types are still available on npm though.
For example, the endpoints for v7.52.0:
js https://developer.api.autodesk.com/modelderivative/v2/viewers/7.52.0/viewer3D.min.js
css https://developer.api.autodesk.com/modelderivative/v2/viewers/7.52.0/style.min.css
Steps
1. Add type information from npm
Firstly, if you are using typescript, you can still install the viewer types from npm with:
yarn add -D #types/forge-viewer (check/add specific version to match the version of the script from the cdn - you can verify in your package.json)
2. Sample ViewingContext.tsx
In your React code you may wish to create a React Context to manage the the script downloading for you. This example is based on next.js:
import React, { PropsWithChildren, useEffect, useState } from "react";
import Script from "next/script";
export const ViewingContext = React.createContext({
initialized: false,
});
export interface ViewingContextProps {
options: Autodesk.Viewing.InitializerOptions;
}
// Place a single global ViewingContextProvider component around the common root of all your Autodesk.Viewing (LMV) components.
// https://forge.autodesk.com/en/docs/viewer/v7/developers_guide/overview/
export const ViewingContextProvider = ({
options,
children,
}: PropsWithChildren<ViewingContextProps>): JSX.Element => {
const [scriptLoaded, setScriptLoaded] = useState(
typeof window !== "undefined" &&
window.Autodesk?.Viewing?.Initializer !== undefined
);
const [initialized, setInitialized] = useState(false);
useEffect(() => {
if (scriptLoaded && !initialized) {
Autodesk.Viewing.Initializer(options, () => setInitialized(true));
}
}, [options, initialized, scriptLoaded]);
return (
<ViewingContext.Provider value={{ initialized }}>
<link
rel="stylesheet"
href="https://developer.api.autodesk.com/modelderivative/v2/viewers/7.52.0/style.min.css"
type="text/css"
/>
<Script
async
src="https://developer.api.autodesk.com/modelderivative/v2/viewers/7.52.0/viewer3D.min.js"
onLoad={(): void => setScriptLoaded(true)}
/>
{children}
</ViewingContext.Provider>
);
};
3. Sample Viewer.tsx Component
Only mount this component as a child of the ViewingContext. You can also modify/replace this component with the ExtendedGuiViewer3D you mentioned.
import React, { useContext, useEffect, useRef } from "react";
import { ViewingContext } from "./ViewingContext";
export interface ViewerProps {
config?: Autodesk.Viewing.Viewer3DConfig;
onLoaded?: (viewer: Autodesk.Viewing.GuiViewer3D) => void;
onError?: (code: number) => void;
}
// Thin wrapper around https://forge.autodesk.com/en/docs/viewer/v7/developers_guide/overview/
// Add your own imperative hook code after GuiViewer object is loaded with the onLoaded callback.
// Place inside a relative layout div.
export const Viewer = ({
config,
onLoaded,
onError,
}: ViewerProps): JSX.Element => {
const { initialized: viewingContextInitialized } = useContext(ViewingContext);
const viewerDivRef = useRef<HTMLDivElement>(null);
const viewer = useRef<Autodesk.Viewing.GuiViewer3D>();
// Viewer imperative loading code
useEffect(() => {
if (viewingContextInitialized && !viewer.current && viewerDivRef.current) {
viewer.current = new Autodesk.Viewing.GuiViewer3D(
viewerDivRef.current,
config
);
const startedCode = viewer.current.start();
if (startedCode > 0) {
onError && onError(startedCode);
return;
}
if (onLoaded) onLoaded(viewer.current);
}
}, [config, onLoaded, onError, viewingContextInitialized]);
// Viewer destructor
useEffect(() => {
return (): void => {
if (viewer.current) {
viewer.current.finish();
}
};
}, []);
return (
<div
style={{
position: "absolute",
width: "100%",
height: "100%",
overflow: "hidden",
}}
>
<div
style={{
margin: 0,
width: "100%",
height: "100%",
}}
ref={viewerDivRef}
/>
</div>
);
};
Hope this answers your question!
so after months of fighting, these are the two options I've come up with so far.
option 1: dirty mock everything
there's a few #ts-ignore, because I dont want to mock out the whole package. i'll only mock the parts my application uses.
you could to type assertion like global.THREE = {...} as unknown as typeof THREE too. whatever floats your boat.
// setupTests.ts
// NOP_VIEWER global is not part of the #types declaration, so we need to tell typescript that there will be a global
declare global {
namespace NodeJS {
interface Global {
NOP_VIEWER: ExtendedGuiViewer3DTypes;
}
}
}
global.Autodesk = {
// #ts-ignore
Viewing: {
GuiViewer3D: jest.fn(),
Extension: jest.fn(),
ToolInterface: jest.fn(),
},
};
// #ts-ignore
global.THREE = {
Color: jest.fn(),
Vector4: jest.fn(),
};
global.NOP_VIEWER = {
disableSelection: jest.fn(),
resize: jest.fn(),
// #ts-ignore
model: {
getExternalIdMapping: (successCallback: any, _: any) => {
successCallback({ 'test-guid': 1 });
},
},
clearThemingColors: jest.fn(),
setThemingColor: jest.fn(),
isLoadDone: () => true,
isolate: jest.fn(),
};
option 2: download and require
As Autodesk Developer Adam Nagy pointed out, you probably wont get sent to jail, if you download the script file and require it locally for your tests only. (note that this is just a "probably")
keep in mind that even if you require the file, you still have to mock NOP_VIEWER as this global is only available after initializing the viewer (which you dont want to do in your tests)
// setupTests.ts
// replace the mocks of `Autodesk` and `THREE` with this require.
require('./vendors/Autodesk/viewer3D.min');
in my tests i can then use the jest spies on NOP_VIEWER
expect(NOP_VIEWER.clearThemingColors).toHaveBeenCalled();

Accessing Vuex Store Before Page Load NuxtJS

Context: I am trying to get Google Maps place data via the place_id on the beforeEnter() route guard. Essentially, I want the data to load when someone enters the url exactly www.example.com/place/{place_id}. Currently, everything works directly when I use my autocomplete input and then enter the route but it does not work when I directly access the url from a fresh tab. I've been able to solve this using the beforeEnter() route guard in traditional Vue, but cannot solve for this using Nuxt. Please help!
Question: How can I access the Vuex Store before a page loads in Nuxt?
Error: Any solution I try (see below) I either end up with a blank page or the page will not load (I think it is stuck in a loop and cannot resolve the Promise).
Attempted Solutions:
Using Middleware like below:
middleware({ store, params }) {
return store.dispatch('myModule/fetchLocation', params.id)
}
Using asyncData like below:
data(){
return{
filteredLocation: {}
}
}
// snip
async asyncData({ store, params }) {
const { data } = await store.dispatch('myModule/fetchLocation', params.id)
return filteredLocation = data
}
I tried looking into fetch, but apparently you no longer have access to context
Example Code:
In one of my store modules:
/* global google */
import Vue from 'vue'
import * as VueGoogleMaps from '~/node_modules/vue2-google-maps/src/main'
Vue.use(VueGoogleMaps, {
load: {
key: process.env.VUE_APP_GMAP_KEY,
libraries: 'geometry,drawing,places'
}
})
export const state = () => ({
selectedLocation: {}
})
export const actions = {
fetchLocation({ commit }, params) {
return new Promise((resolve) => {
Vue.$gmapApiPromiseLazy().then(() => {
const request = {
placeId: params,
fields: [
'name',
'rating',
'formatted_phone_number',
'geometry',
'place_id',
'website',
'review',
'user_ratings_total',
'photo',
'vicinity',
'price_level'
]
}
const service = new google.maps.places.PlacesService(
document.createElement('div')
)
service.getDetails(request, function(place, status) {
if (status === 'OK') {
commit('SET_PLACE', place)
resolve()
}
})
})
})
}
}
export const mutations = {
SET_PLACE: (state, selection) => {
state.selectedInstructor = selection
}
}
EDIT: I already have it in a plugin named google-maps.js and in my nuxt.config.js file I have:
plugins: [
{ src: '~/plugins/google-maps.js' }
]
//
//
build: {
transpile: [/^vue2-google-maps.js($|\/)/],
extend(config, ctx) {}
}
Using Middleware is how we can access Vuex before page loads. try putting the configuration part in a custom Nuxt plugin.
Create a file in Plugins folder (you can name it global.js).
Put this
import Vue from 'vue'
import * as VueGoogleMaps from '~/node_modules/vue2-google-maps/src/main'
Vue.use(VueGoogleMaps, {
load: {
key: process.env.VUE_APP_GMAP_KEY,
libraries: 'geometry,drawing,places'
}
})
in global.js.
Then add the plugin in nuxt.config.js like this.
plugins: [
'~/plugins/global.js'
]
Also, make sure you're using underscore before 'page_id' name in your folder structure.

Gulp 4 - How to generate dynamic tasks based on two arrays

I'm building an e-mail generation pipeline (multiple templates) using nunjucks and json translation files. This means I need to loop over the multiple templates and the translation files, however I can't seem to get it working.
Tried adding another loop inside the templates.map(), but that doesn't seem to be working (or I'm doing it completely wrong ofcourse). It almost works, but it crashes at some point, generating only a few of the templates. The first template works, but it crashes at the second template:
The following tasks did not complete: <anonymous>
Did you forget to signal async completion?
source: https://cobwwweb.com/dynamic-tasks-gulp-4
var templates = [];
var languages = ["nl", "en"];
function generateTemplates(done) {
const tasks = templates.map((template) => {
return () => {
const langs = languages.map((lang) => {
return () =>
gulp.src(`source/templates/${template}`)
.pipe(data(function () {
return require(`./source/translations/${lang}/${template.split('.')[0] }.json`);
}))
.pipe(nunjucksRender({
path: ['source/partials']
}))
.pipe(gulp.dest('dist/' + lang));
});
return gulp.series(...langs, (seriesDone) => {
seriesDone();
})();
}
});
return gulp.series(...tasks, (seriesDone) => {
seriesDone();
done();
})();
}
I also tried generating tasks using 2 for-loops, but this only generates the last template of the array of the last language in the array (example: only en/template2 will be generated correctly). I do see in the console that the tasks are starting and finishing, but I don't see them anywhere. Maybe the loop is finished mush faster than the generation of tasks? :
var templates = fs.readdirSync('./source/templates');
var languages = ["nl", "en"];
for (var lang of languages) {
for (var template of templates) {
gulp.task(`${lang}-${template}`, function (done) {
return gulp.src(`source/templates/${template}`)
.pipe(data(function () {
return require(`./source/translations/${lang}/${template.split('.')[0]}.json`);
}))
.pipe(nunjucksRender({
path: ['source/partials']
}))
.pipe(gulp.dest(`dist/${lang}`));
});
tasks.push(`${lang}-${template}`);
}
}
gulp.task('genlang', gulp.series(tasks));
My folder structure:
/dist
/source
--/partials
--/templates
--/template1.html
--/template2.html
--/translations
--/en
--/template1.json
--/template2.json
--/nl
--/template1.json
--/template2.json
Fixed it myself, I needed to have some done cb's in the returns:
function generateTemplates(done) {
const tasks = templates.map((template) => {
return (doneTasks) => {
const langs = languages.map((lang) => {
return (doneLanguages) => {
gulp.src(`source/templates/${template}`)
.pipe(data(() => require(`./source/translations/${lang}/${template.split('.')[0]}.json`)))
.pipe(nunjucksRender({
path: ['source/partials']
}))
.pipe(gulp.dest('./dist/' + lang));
doneLanguages();
}
});
return gulp.parallel(...langs, (seriesDone) => {
seriesDone();
doneTasks();
})();
};
});

Resolving an ES6 module imported from a URL with Rollup

It is perfectly valid to import from a URL inside an ES6 module and as such I've been using this technique to reuse modules between microservices that sit on different hosts/ports:
import { authInstance } from "http://auth-microservice/js/authInstance.js"
I'm approaching a release cycle and have started down my usual path of bundling to IIFEs using rollup. Rollup doesn't appear to support es6 module imports from URLs, I think it should as this is allowed in the spec :(
module-name
The module to import from. This is often a relative or absolute path name to the .js file containing the module. Certain bundlers may permit or require the use of the extension; check your environment. Only single quotes and double quotes Strings are allowed. (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import)
I've dug through the interwebs for an hour now and have come up with nothing. Has anybody seen a resolver similar to rollup-plugin-node-resolve for resolving modules from URLs?
I had to move on from this quickly so ended up just writing a skeleton of a rollup plugin. I still feel that resolving absolute paths should be a core feature of rollup.
Updated snippet
We have been using this to transpile production code for several of our apps for a considerable amount of time now.
const fs = require('fs'),
path = require('path'),
axios = require("axios")
const createDir = path => !fs.existsSync(path) && fs.mkdirSync(path)
const mirrorDirectoryPaths = async ({ cacheLocation, url }) => {
createDir(cacheLocation)
const dirs = [], scriptPath = url.replace(/:\/\/|:/g, "-")
let currentDir = path.dirname(scriptPath)
while (currentDir !== '.') {
dirs.unshift(currentDir)
currentDir = path.dirname(currentDir)
}
dirs.forEach(d => createDir(`${cacheLocation}${d}`))
return `${cacheLocation}${scriptPath}`
}
const cacheIndex = {}
const writeToDiskCache = async ({ cacheLocation, url }) => {
//Write a file to the local disk cache for rollup to pick up.
//If the file is already existing use it instead of writing a new one.
const cached = cacheIndex[url]
if (cached) return cached
const cacheFile = await mirrorDirectoryPaths({ cacheLocation, url }),
data = (await axiosInstance.get(url).catch((e) => { console.log(url, e) })).data
fs.writeFileSync(cacheFile, data)
cacheIndex[url] = cacheFile
return cacheFile
}
const urlPlugin = (options = { cacheLocation }) => {
return {
async resolveId(importee, importer) {
//We importing from a URL
if (/^https?:\/\//.test(importee)) {
return await writeToDiskCache({ cacheLocation: options.cacheLocation, url: importee })
}
//We are importing from a file within the cacheLocation (originally from a URL) and need to continue the cache import chain.
if (importer && importer.startsWith(options.cacheLocation) && /^..?\//.test(importee)) {
const importerUrl = Object.keys(cacheIndex).find(key => cacheIndex[key] === importer),
importerPath = path.dirname(importerUrl),
importeeUrl = path.normalize(`${importerPath}/${importee}`).replace(":\\", "://").replace(/\\/g, "/")
return await writeToDiskCache({ cacheLocation: options.cacheLocation, url: importeeUrl })
}
}
}
}
This plugin together with the following config works for me:
https://github.com/mjackson/rollup-plugin-url-resolve
import typescript from "#rollup/plugin-typescript";
import urlResolve from "rollup-plugin-url-resolve";
export default {
output: {
format: "esm",
},
plugins: [
typescript({ lib: ["es5", "es6", "dom"], target: "es5" }),
urlResolve(),
],
};
You can remove the TypeScript plugin obviously.

How to pass const to multiple components / Spliting React-Redux-Router files

I am creating a Spotify app with its API. I want 4 views (like '/', 'nowPlaying', 'favouriteArtists', 'favouriteSongs').
I need to setAccessToken for using functions like getMyCurrentPlaybackState() in every new page, right?. Idk if I need to if(params.access_token){spotifyWebApi.setAccessToken(params.access_token)} in every container that will use functions like getMyCurrentPlaybackState(). I was thinking of creating a Spotify.jsx container that handle the store of the Spotify Object (which is used in the token and in every container that use spotify functions). But with this Spotify.jsx i don't know either if it is a good approach nor how to connect its needed spotifyWebApi const to every container file and token file.
For better understanding of my idea: I would create a Token.jsx that has getHashParams() and a Playing.jsx that has getNowPlaying(). Every one needs the spotifyWebApi const.
import React, { Component } from 'react';
import Spotify from 'spotify-web-api-js';
const spotifyWebApi = new Spotify();
class App extends Component {
constructor(){
super();
const params = this.getHashParams();
this.state = {
loggedIn: params.access_token ? true : false,
nowPlaying: {
name: 'Not Checked',
image: ''
}
}
if (params.access_token){
spotifyWebApi.setAccessToken(params.access_token)
}
}
getHashParams() {
var hashParams = {};
var e, r = /([^&;=]+)=?([^&;]*)/g,
q = window.location.hash.substring(1);
while ( e = r.exec(q)) {
hashParams[e[1]] = decodeURIComponent(e[2]);
}
return hashParams;
}
getNowPlaying(){
spotifyWebApi.getMyCurrentPlaybackState()
.then((response) => {
this.setState({
nowPlaying: {
name: response.item.name,
image: response.item.album.images[0].url
}
})
})
}
}
Your title mentions Redux, but I don't see your code utilizing it. With Redux, you could get the access_token and then store it in state. This will allow you to use it in any Redux connected component.
Also, with Redux, you can use Redux Thunk (or similar) middleware that will allow you to use Redux actions to call an API. So then you would just write the different API calls as Redux actions, which would allow you to call them from any component, and have the results added to your Redux store (which again, can be used in any Redux connected component).
So, for example, your getNowPlaying() function could be an action looking something like this:
function getNowPlaying() {
return function (dispatch, getState) {
// get the token and init the api
const access_token = getState().spotify.access_token
spotifyWebApi.setAccessToken(access_token)
return spotifyWebApi.getMyCurrentPlaybackState().then((response) => {
dispatch({
type: 'SET_NOW_PLAYING',
name: response.item.name,
image: response.item.album.images[0].url
})
})
}
}
Note: You'll need to configure the Redux reducer for "spotify" (or however you want to structure your store) to store the data you need.
So, you could then call getNowPlaying() from any component. It stores the results in the redux store, which you could also use from any connected component. And you can use the same technique of getting the access_token from the store when needed in the actions.
Alternatively, if you didn't want to use Redux, you could provide context values to all child components, using React's Context features. You could do this with that token that each component would need in your setup. But Redux, in my opinion, is the better option for you here.
Instead of passing this const to other components, I would create a SpotifyUtils.jsx and inside it declare the const. And in this helper file I would export functions so other components can use them.
For example:
import Spotify from 'spotify-web-api-js';
const spotifyWebApi = new Spotify();
let token = null
export function isLoggedIn() {
return !!token
}
export function setAccessToke(_token) {
token = _token;
spotifyWebApi.setAccessToken(_token);
}
export function getNowPlaying(){
return spotifyWebApi.getMyCurrentPlaybackState()
.then((response) => {
return {
name: response.item.name,
image: response.item.album.images[0].url
}
})
}
So that in the components you can use them like so:
import React, { Component } from 'react';
import {
isLoggedIn,
setAccessToken,
getNowPlaying,
} from 'helpers/SpotifyUtils'
class App extends Component {
constructor(){
super();
this.state = {
loggedIn: isLoggedIn(),
nowPlaying: {
name: 'Not Checked',
image: ''
}
}
getHashParams() {
var hashParams = {};
var e, r = /([^&;=]+)=?([^&;]*)/g,
q = window.location.hash.substring(1);
while ( e = r.exec(q)) {
hashParams[e[1]] = decodeURIComponent(e[2]);
}
return hashParams;
}
componentDidMount() {
if (!this.state.loggedIn) {
const params = this.getHashParams();
if (params.access_token) {
setAccessToken(params.access_token)
getNowPlaying()
.then(nowPlaying => this.setState({ nowPlaying }))
}
}
}
}
This will enable your spotifyWebApi const to be reused in any component you import the helper functions. I am particularly found of this pattern, creating utils or helpers in a generic fashion so that you can reuse code easily. Also if spotify Web Api releases a breaking change, your refactor will be easier because you will only need to refactor the SpotifyUtils.jsx file since it will be the only file using import Spotify from 'spotify-web-api-js'