Websockets in Sapper - javascript

I have a readable store in Svelte that looks like this:
const state = {};
export const channels = readable(state, set => {
let st = state;
let socket = new WebSocket("ws://127.0.0.1:5999");
socket.onmessage = function (event) {
var datastr = event.data.split(':');
st[datastr[0]].value = datastr[1];
st[datastr[0]].timestamp = Date.now();
set(st)
};
return () => {
socket.close()
}
});
When I import it to my Svelte App works. But if I put that App.svelte as my index.svelte running on Sapper, it doesnt work at first. It says error 500 websocket is not defined. Once I reload the page in the browser start to work...
I have try to parse a function that creates the store instead:
export const getChannel = () => {
// here my store
return {...store}
}
and then creating the store inside a onMount() like this:
onMount( ()=> {
const channel = getChannel();
});
But doesnt seem to do the trick... What do I miss?
Note: If a just replace the store by a simple writable, and create the websocket onMount(), it works without any problem. I just only wanted to put all the communication inside the store as a readable...

In Sapper, code in components (or imported into components) is executed in Node during server-side rendering unless it's put inside onMount (which doesn't run on the server, because there's no 'mounting' happening) or an if (process.browser) {...} block, or something equivalent.
That includes things like references to $channels causing channels.subscribe(...) to be called during initialisation.
Since there's no WebSocket global in Node, creating that subscription will fail. The simplest workaround is probably a simple feature check:
const state = {};
export const channels = readable(state, (set) => {
if (typeof WebSocket === 'undefined') return;
let st = state;
let socket = new WebSocket("ws://127.0.0.1:5999");
socket.onmessage = function (event) {
var datastr = event.data.split(":");
st[datastr[0]].value = datastr[1];
st[datastr[0]].timestamp = Date.now();
set(st);
};
return () => {
socket.close();
};
});

Related

Uncaught ReferenceError in DevTools for global variables [duplicate]

This question already has answers here:
How can I use modules in the browser, but also refer to variables and functions from within DevTools?
(4 answers)
Closed yesterday.
Update:
Looks like by importing another JS file and using the ES6 Modules, devtools compiles a bit different (will need to dive into learning more). The variables I thought were global were not in the module. Was able to fix this by assigning them to:
window.variable_name = variable_name
This allowed me to call the variables from devtools.
I have a project I am working on to practice a few JS skills. For the most part I have everything up and running but have come across an issue I can not seem to remedy.
I have an app.js file that is linked in my index.html. When running the live server from VS code everything works as it should, but when I open devtools to check some of the variables (global) & functions I get "Uncaught referenceError: variable_name is not defined" with a VM#:# reference.
Up to today, I have been able to call the variables from console without a problem.
I have 12 global variables, and all but 1 (submit) have this issue.
App.js:
import { autocomplete } from "./autocomplete.js";
// ######## GLOBAL VARIABLES ############
//------- Selectors ----------
const container = document.querySelector('.search-container');
const searchElement = document.getElementById('search');
const searchInput = document.querySelector('input[name="search-bar"]');
const submit = document.getElementById('search-bar');
const loader = document.querySelector('.loader');
const results = document.querySelector('.results');
const resetButton = document.querySelector('.reset');
const errorMessage = document.querySelector('.error');
const heart = document.querySelector('#favorite');
//------- Arrays & Objects ----------
let names = [];
let pokemon = {};
let bgImages = ["./resources/images/forest_background.jpg", "./resources/images/field.jpg", "./resources/images/galar-scenery.png", "./resources/images/night.jpg", "./resources/images/training.jpg", "./resources/images/poke-background.webp"];
//########## Grab & Store Pokemon Names for Autocomplete ##########
async function loadPokeNames() {
try {
const response = await fetch ('https://pokeapi.co/api/v2/pokemon?limit=250');
if (response.ok) {
const jsonResponse = await response.json();
// console.log(jsonResponse)
for (const poke of jsonResponse.results){
names.push(poke.name);
}
}
// throw new Error('Request Failed!')
} catch(error){
console.log(error);
}
}
//############ Search Function ###############
async function searchPokemon(e) {
e.preventDefault();
let pokeSearchValue = e.srcElement[0].value.toLowerCase();
searchElement.hidden = true;
loader.hidden = false;
try {
const pokeResponse = await fetch(`https://pokeapi.co/api/v2/pokemon/${pokeSearchValue}`);
if (pokeResponse.ok) {
const pokeJSON = await pokeResponse.json();
// Assign Values to Pokemon Object
pokemon.name = pokeJSON["name"];
pokemon.img = pokeJSON["sprites"]["other"]["official-artwork"]["front_default"];
pokemon.hp = pokeJSON["stats"][0]["base_stat"];
pokemon.attack = pokeJSON["stats"][1]["base_stat"];
pokemon.speed = pokeJSON["stats"][5]["base_stat"];
pokemon.defense = pokeJSON["stats"][2]["base_stat"];
pokemon.special_attack = pokeJSON["stats"][3]["base_stat"];
pokemon.special_defense = pokeJSON["stats"][4]["base_stat"];
console.log(pokemon);
createPokeCard(pokemon);
} else {
throw new Error("Something Went Wrong.");
}
} catch (error) {
loader.hidden = true;
errorMessage.hidden = false;
resetButton.hidden = false;
console.log(error);
}
}
// ####### Generates the Pokemon Card #########
function createPokeCard(object) {
const pokeName = document.querySelector('#poke-name p');
const pokeHP = document.querySelector('#hp');
const pokeImg = document.querySelector('#poke-image img');
const pokeAttack = document.querySelector('#attack .num');
const pokeSpeed= document.querySelector('#speed .num');
const pokeDefense = document.querySelector('#defense .num');
const pokeSpecialA = document.querySelector('#special-attack .num');
const pokeSpecialD = document.querySelector('#special-defense .num');
const backgroundImage = document.querySelector('#poke-image')
// Assign values to Results Card
backgroundImage.style.backgroundImage = `url('${bgImages[Math.floor(Math.random() * 6)]}')`;
pokeName.textContent = object.name;
pokeHP.textContent = `${object.hp} HP`;
pokeImg.src = object.img;
pokeAttack.innerText = object.attack;
pokeDefense.textContent = object.defense;
pokeSpeed.textContent = object.speed;
pokeSpecialA.textContent = object.special_attack;
pokeSpecialD.textContent = object.special_defense;
setTimeout(() => {
loader.hidden = true;
results.hidden = false;
resetButton.hidden = false;
}, 3000)
}
// ####### Resets Search & Card #########
function resetSearch() {
searchInput.value = '';
resetButton.hidden = true;
results.hidden = true;
searchElement.hidden = false;
errorMessage.hidden = true;
for (const att in pokemon){
delete pokemon[att];
}
}
//######## Favorite Functions ###########
function hoverFav() {
this.src = '../resources/images/heartline-fill.png';
}
function hoverOutFav() {
this.src = '../resources/images/heartline.png';
}
// ########### EVENTS ##############
window.onload = loadPokeNames;
autocomplete(searchInput, names)
heart.addEventListener('mouseenter', hoverFav);
heart.addEventListener('mouseout', hoverOutFav);
resetButton.addEventListener('click', resetSearch);
submit.addEventListener('submit', searchPokemon);
Linked (correct path) script tag:
<script src="./script/app.js" type="module"></script>
Any help or a point in the right direction would be amazing.
Tried calling global variables from devtools. Got an uncaught referenceError message.
Alright, so I have made a bit of digging around and it turns out the reason this happens is indeed because you're setting the type of the script as a module. Scripts that are run as modules are under strict mode by default, thus they don't allow access to global variables.
Also, they are made to run as a separate scripts(modules) and so any variables defined or imported into them are private by default and are not accessible to the global scope.
As for the error, in normal standard JS scripts, the access of a variable not defined properly(without let/const) will not throw an error, under strict mode however, it converts it into an error and throws it.
Take a look here for more info on differences between standard & module scripts. Also here for info on strict mode.

How to eval a function that use external modules in a web worker

I am trying to implement a simple library to offload given tasks to a pool of web-workers. To do so, I am using the eval operator inside the worker to evaluate stringified functions that come from the main thread. To do so I am leveraging the webpack's plugin worker-plugin to automatically bundle workers inside the application.
First of all, I format the task to normalize arrow functions, class members, and standard functions
function format_task(task: string): string
{
if(task.indexOf('=>') !== -1)
return task;
if(task.indexOf('function') !== -1)
return `(function ${task.slice('function'.length).trim()})`;
return `(function ${task})`;
}
Then, I simply eval the resulting function calling it:
self.onmessage = (event: MessageEvent) =>
{
const {task, data} = event.data;
const formatted_task = format_task(task);
const runnable_task = eval(formatted_task);
const result = await runnable_task(...data);
self.postMessage(result);
}
The main usage would be something like:
const pool = new WorkerPool(new Worker('./worker.ts'), 10);
const task = (a: number, b: number) => a + b;
const worker_task = await pool.create_task(task);
const result = await worker_task.run(42, 12);
So far, so good. The WorkerPool instance is handling a pool of 10 workers, the function gets executed in one of them, and the result gets returned to the main thread.
The problem comes in when I try to use an external dependency inside the task as
import {toUpper} from 'lodash';
const pool = new WorkerPool(new Worker('./worker.ts'), 10);
const task = (a: number, b: number) =>
{
return toUpper('Result is ') + (a + b));
}
const worker_task = await pool.create_task(task);
const result = await worker_task.run(42, 12);
In this case, I get that lodash_1.toUpper is not defined inside the worker. The only solution would be to manually import the same function also inside the worker environment (it works). However, I would like to keep the web-worker implementation as clean as possible for the final user. Something like:
const context: Worker = self as any;
(async () =>
{
const pool_worker = new PoolWorker(context.postMessage.bind(context));
context.onmessage = pool_worker.on_message.bind(pool_worker);
await pool_worker.run();
})().catch(console.error);
I tried importing the needed libraries inside the main worker file but it does not work.
import {toUpper} from 'lodash';
const context: Worker = self as any;
(async () =>
{
const pool_worker = new PoolWorker(context.postMessage.bind(context));
context.onmessage = pool_worker.on_message.bind(pool_worker);
await pool_worker.run();
})().catch(console.error);
It only works if I import it inside the PoolWorker class.
Can you think of a way to "pass" dependencies to the PoolWorker instance letting it importing it through webpack without writing a custom webpack loader/plugin?
Thank you in advance

How to run server-sent events in svelte component in sapper

I have a svelte component named [symbol].svelte in which I want to initiate a connection to a streaming service to receive server-sent events. I've not found a way to do this successfully.
Since EventSource only runs in the browser, I initialized it in the onMount function like so:
<script>
export let quote;
let sse = {};
onMount(async () => {
sse = new EventSource(`https://myurl.com?symbol=${quote.symbol}`);
sse.onmessage = (event) => {
let response = JSON.parse(event.data);
if(!response.length) return;
quote = response[0];
}
});
onDestroy(() => {
if(sse.readyState && sse.readyState === 1) {
sse.close();
}
})
</script>
<div>{quote.symbol}</div>
This works fine, except when I navigate to another route that uses the same component- since the component doesn't unmount and remount, onMount() doesn't fire and thus doesn't instantiate a new SSE request. I don't know of any way to easily force the component to remount, which would be simplest (relevant github issue here)
Another try was using a reactive statement like so:
<script>
export let quote;
let sse = {};
$: {
if(process.browser === true) { //again, this stuff won't run on the server
if(sse.readyState && sse.readyState === 1) {
sse.close();
}
sse = new EventSource(`https://myurl.com?symbol=${quote.symbol}`);
}
}
sse.onmessage = (event) => {
let response = JSON.parse(event.data);
quote = response[0];
console.log(quote);
}
</script>
<div>{quote.symbol}</div>
When changing routes, the quote variable changed, thus triggering the reactive statement to kill the existing SSE and instantiate a new one. Exceptthe onmessage handler wouldn't fire, probably because the onmessage handler gets attached before the eventsource object is created.
Last take was to try with the onmessage handler in the reactive statement like so:
<script>
export let quote;
let sse = {};
$: {
if(process.browser === true) { //again, this stuff won't run on the server
if(sse.readyState && sse.readyState === 1) {
sse.close();
}
sse = new EventSource(`https://myurl.com?symbol=${quote.symbol}`);
sse.onmessage = (event) => {
let response = JSON.parse(event.data);
quote = response[0];
console.log(quote);
}
}
}
</script>
<div>{quote.symbol}</div>
The problem here is that since quote gets reassigned as a product of the onmessage handler, the reactive statement keeps firing circularly.
At this point I'm at a loss, any input would be appreciated!
It sounds like you want to use {#key ...}, which causes its contents to be torn down and recreated when the value changes, including components:
{#key quote}
<!-- destroyed and recreated whenever `quote` changes -->
<Quote {quote}/>
{/key}
Docs here: https://svelte.dev/docs#key
Incidentally, using onDestroy is unnecessary if it's only used to clean up work that happens in onMount:
onMount(() => {
const sse = new EventSource(`https://myurl.com?symbol=${quote.symbol}`);
sse.onmessage = (event) => {
let response = JSON.parse(event.data);
if(!response.length) return;
quote = response[0];
}
};
return () => {
if(sse.readyState === 1) {
sse.close();
}
});
});
This is better because you don't have the top-level sse variable, and because the returned cleanup function only needs in the browser, you don't need to have the placeholder ssr = {} assignment or check for sse.readyState.

Passing socket.io instance to JS object constructor

I read this question and attempted to do the solution, however I am trying to pass my instance of io to an object constructor instead of a class. I originally attempted to do something like...
//index.js
const {CONNECTION, CREATE_ROOM} = require('./SignalTypes')
const app = require('express')()
const server = require('http').Server(app)
const io = require('socket.io')(server)
const Lobbies = require('./lobby')
let lobbies = new Lobbies(io)
io.of('/menu').on(CONNECTION, (socket) => {
console.log(`User connected to main menu`)
socket.on(CREATE_ROOM, () => {
const roomKey = lobbies.createLobby()
socket.emit(ROOM_CREATED, roomKey)
})
...
})
And my Lobbies file looks like...
//lobby.js
const shortid = require('shortid')
function Lobbies(io) {
this.io = io;
this.lobbies = {}
}
Lobbies.prototype.createLobby = () => {
let roomKey = shortid.generate()
//create namespace for new lobby
const lobbyNamespace = this.io.of(`/${roomKey}`) // issue
this.lobbies[roomKey] = new Lobby(roomKey, lobbyNamespace)
return roomKey
}
//Lobby object constructor defined later
...
module.exports = Lobbies
However I keep running into errors in which it says io is undefined at the line
//lobby.js
const lobbyNamespace = this.io.of(`/${roomKey}`)
//TypeError: Cannot read property 'of' of undefined
I was wondering if there's a way to pass my io object to my object constructor without having to change it into an ES6 class or something. Any suggestions?
You are losing this reference when using arrow function syntax. I don't know why do you want to use old, hard-to-read syntax, but if you want that instead of class you should do:
Lobbies.prototype.createLobby = function() {
let roomKey = shortid.generate()
//create namespace for new lobby
const lobbyNamespace = this.io.of(`/${roomKey}`) // issue
this.lobbies[roomKey] = new Lobby(roomKey, lobbyNamespace)
return roomKey
}

With flux, how do I get the right Store called

I'm new to react/flux architecture, and I'm missing something...I think. I have two Stores, SubjectsStore.js and WorkDoneStore.js with an AppActions which does the dispatch (code snippets all below). I'm under the impression that any Store that registers with the AppDispatcher will get notice of the event, and it is incumbent on each store to handle the proper action types. There doesn't seem to be any other way of controlling which Store gets called. In my case, I've gotten as far as getting one the SubjectStores registration to be called, but my WorkDoneStore is not getting called. What am I overlooking / doing wrong.
AppActions.js
import AppDispatcher from './AppDispatcher.js';
import WorkDoneConstants from '../constants/WorkDoneConstants.js';
import SubjectConstants from '../constants/SubjectConstants.js';
var AppActions = {
addWorkDoneItem:function(item){
console.log("In app actions addWorkDone");
console.log(WorkDoneConstants.WORKDONE_INSERT);
AppDispatcher.dispatch({
actionType:WorkDoneConstants.WORKDONE_INSERT,
item:item
})
}
}
module.exports = AppActions;
SubjectsStore.js
var AppDispatcher = require('../dispatcher/AppDispatcher');
var SubjectConstants = require('../constants/SubjectConstants');
var EventEmitter = require('events').EventEmitter;
...
AppDispatcher.register(function(action) {
var text;
console.log("why am I in the subjectStore?");
console.log(action.actionType);
console.log(action.item);
switch(action.actionType) {
case SubjectConstants.SUBJECT_CREATE:
text = action.text.trim();
...
WorkDoneStore.js
...
AppDispatcher.register(function(action) {
var text;
console.log("In WorkDoneStore");
console.log(action);
switch(action.actionType) {
case WorkDoneConstants.WORKDONE_INSERT:
item = action.item;
if (item.subject !== '') {
create(item);
WorkDoneStore.emitChange();
}
break;
...
My component
...
handleSubmit: function(e){
e.preventDefault();
var item = {
subject:this.state.subject,
workDone:this.state.workDone,
minutes:this.state.totalMinutes,
startStop:this.state.startStop,
};
console.log("before AppActions.");
AppActions.addWorkDoneItem(item);
},
...
In looking through my Webpack output I noticed that the WorkDoneStore.js wasn't getting included. By forcing it to be included via a call to it, it's now working.

Categories

Resources