Where do websockets fit into the Alt way of doing flux? What should be responsible for translating socket messages into actions and vice versa?
Components
Stores
Containers
Actions
Sources
EDIT:
Here's what I have now which seems to work, but please post an answer if you have a flux-appropriate way of doing things.
var alt = require('../alt');
var UserActions = require('../actions/UserActions');
var socket = io();
socket.on("user", function(user) {
UserActions.addUser(user);
});
class UserStore {
constructor() {
this.users = [{id:1, name: "michael"}, { id:2, name: "lauren"}];
this.bindListeners({
handleUpdateUsers: UserActions.UPDATE_USERS,
handleAddUser: UserActions.ADD_USER
});
}
handleUpdateUsers(users) {
this.users = users;
}
handleAddUser(user) {
this.users.push(user);
}
}
module.exports = alt.createStore(UserStore, 'UserStore');
Related
I have microservice-based system which works with documents. Service publishes DocflowErrorMq, ImportedDocflowMq events, and other services are subscribed to these events. Critical service DocflowRegistry should process messages quickly, so we have to introduce multiple consumers. On the other hand message order shouldn't be broken and competing consumer doesn't suite. Consistent hash exchange distributes messages by routing key equals to document id, messages related to one document goes to one queue. So, we have simple manual scaling. I can't create binding between MqModels.Docflows:ImportedDocflowMq and docflow-process-dr exchanges (marked red on Diagram). Is it possible to create it with MassTransit?
DocflowRegistry service config:
services.AddMassTransit(x =>
{
x.AddConsumer<DocflowSendingErrorTestConsumer>();
x.AddConsumer<DocflowImportTestConsumer>();
x.UsingRabbitMq((context, cfg) =>
{
var virtualHost = configuration["RabbitMq:Settings:VirtualHost"] ?? "/";
cfg.Host(configuration["RabbitMqHost"], virtualHost, h =>
{
h.Username(configuration["RabbitMqUserName"]);
h.Password(configuration["RabbitMqPassword"]);
});
cfg.ReceiveEndpoint("docflow.process-1.docflowregistry", e =>
{
e.ConfigureConsumer<DocflowSendingErrorTestConsumer>(context);
e.ConfigureConsumer<DocflowImportTestConsumer>(context);
e.Bind("docflow-process-dr", x =>
{
x.Durable = true;
x.AutoDelete = false;
x.ExchangeType = "x-consistent-hash";
x.RoutingKey = "1";
});
e.ConfigureConsumeTopology = false;
e.SingleActiveConsumer = true;
});
cfg.ReceiveEndpoint("docflow.process-2.docflowregistry", e =>
{
e.ConfigureConsumer<DocflowSendingErrorTestConsumer>(context);
e.ConfigureConsumer<DocflowImportTestConsumer>(context);
e.Bind("docflow-process-dr", x =>
{
x.Durable = true;
x.AutoDelete = false;
x.ExchangeType = "x-consistent-hash";
x.RoutingKey = "1";
});
e.ConfigureConsumeTopology = false;
e.ConcurrentMessageLimit = 1;
e.SingleActiveConsumer = true;
});
});
});
Config of TodoList service:
services.AddMassTransit(x =>
{
x.AddConsumer<DocflowSendingErrorTestConsumer>();
x.AddConsumer<DocflowImportTestConsumer>();
x.UsingRabbitMq((context, cfg) =>
{
var virtualHost = configuration["RabbitMq:Settings:VirtualHost"] ?? "/";
cfg.Host(configuration["RabbitMqHost"], virtualHost, h =>
{
h.Username(configuration["RabbitMqUserName"]);
h.Password(configuration["RabbitMqPassword"]);
});
cfg.ReceiveEndpoint("docflow-process-todolist", e =>
{
e.ConfigureConsumer<DocflowSendingErrorTestConsumer>(context);
e.ConfigureConsumer<DocflowImportTestConsumer>(context);
e.SingleActiveConsumer = true;
});
});
});
Publish code:
var endPoint = await _massTransitBus.GetPublishSendEndpoint<DocflowErrorMq>();
var docflowGuid = Guid.NewGuid();
await endPoint.Send(new DocflowErrorMq
{
DocflowId = docflowGuid,
AbonentId = Guid.NewGuid()
},
context =>
{
context.SetRoutingKey(docflowGuid.ToString());
});
Create an interface, DocflowProcessDr, and make each of those message contracts published implement it. Then, you can configure the publish topology for that interface in the bus:
cfg.Message<DocflowProcessDr>(x => x.SetEntityName("docflow-process-dr"));
cfg.Publish<DocflowProcessDr>(x =>
{
x.ExchangeType = "x-consistent-hash";
});
Since MassTransit will create a polymorphic topology on the broker, you'll have an exchange-to-exchange binding between the published type and the interface.
Then, just publish the message:
var docflowGuid = Guid.NewGuid();
var endPoint = await _massTransitBus.Publish<DocflowErrorMq>(new DocflowErrorMq
{
DocflowId = docflowGuid,
AbonentId = Guid.NewGuid()
},
context =>
{
context.SetRoutingKey(docflowGuid.ToString());
});
Calling GetPublishSendEndpoint<T>() is weird, don't encourage it.
PROBLEM
Unavailable variant combinations are different from sold-out because customers don't understand it's the selects which make certain combinations 'not possible'.
Shopify's way of handling this is to display 'Unavailable' in the buy button. But customers think this means sold-out when in reality, they've chosen the wrong combination of variants...
The previous JS workarounds to remove unavailable or 'not possible' variants don't work in Shopify 2.0's new default/flagship theme, Dawn because the JS is different.
As far as I can tell, Dawn's variant JS was recently moved from /asstes/variants.js to line 497 in /assets/global.js.
SKILL
My CSS is decent but my JS is lame, I'm a designer sorry.
QUESTIONS
Based on user interaction with the first variant, how do you hide unavailable variants (not sold-out) in Shopify 2.0 Dawn?
How do you make one variant option set a checkbox instead of a radio button or radio?
What's the best way to add custom text as the first option in selects? e.g. 'Choose a size...' or 'Choose a color...' etc. Is it best to hard-code or use JS for this as well?
RESOURCES/EXAMPLES
Here's a pull request which grabs sold-out from the new Dawn JS but I don't understand how to adapt it for 'Unavailable' sorry (which is a different exception from sold-out): https://github.com/Shopify/dawn/pull/105
Here's an example of how to hide unavailable variants in the older Debut theme which doesn’t seem to work in the newer Dawn JS: https://www.youtube.com/watch?v=vspWDu_POYA
Here's a link to the JS gist referenced in that video: https://gist.github.com/jonathanmoore/c0e0e503aa732bf1c05b7a7be4230c61
And finally, here's the new code from Dawn at line 497 in /assets/global.js
class VariantSelects extends HTMLElement {
constructor() {
super();
this.addEventListener('change', this.onVariantChange);
}
onVariantChange() {
this.updateOptions();
this.updateMasterId();
this.toggleAddButton(true, '', false);
this.updatePickupAvailability();
if (!this.currentVariant) {
this.toggleAddButton(true, '', true);
this.setUnavailable();
} else {
this.updateMedia();
this.updateURL();
this.updateVariantInput();
this.renderProductInfo();
}
}
updateOptions() {
this.options = Array.from(this.querySelectorAll('select'), (select) => select.value);
}
updateMasterId() {
this.currentVariant = this.getVariantData().find((variant) => {
return !variant.options.map((option, index) => {
return this.options[index] === option;
}).includes(false);
});
}
updateMedia() {
if (!this.currentVariant || !this.currentVariant?.featured_media) return;
const newMedia = document.querySelector(
`[data-media-id="${this.dataset.section}-${this.currentVariant.featured_media.id}"]`
);
if (!newMedia) return;
const parent = newMedia.parentElement;
parent.prepend(newMedia);
window.setTimeout(() => { parent.scroll(0, 0) });
}
updateURL() {
if (!this.currentVariant) return;
window.history.replaceState({ }, '', `${this.dataset.url}?variant=${this.currentVariant.id}`);
}
updateVariantInput() {
const productForms = document.querySelectorAll(`#product-form-${this.dataset.section}, #product-form-installment`);
productForms.forEach((productForm) => {
const input = productForm.querySelector('input[name="id"]');
input.value = this.currentVariant.id;
input.dispatchEvent(new Event('change', { bubbles: true }));
});
}
updatePickupAvailability() {
const pickUpAvailability = document.querySelector('pickup-availability');
if (!pickUpAvailability) return;
if (this.currentVariant?.available) {
pickUpAvailability.fetchAvailability(this.currentVariant.id);
} else {
pickUpAvailability.removeAttribute('available');
pickUpAvailability.innerHTML = '';
}
}
renderProductInfo() {
fetch(`${this.dataset.url}?variant=${this.currentVariant.id}§ion_id=${this.dataset.section}`)
.then((response) => response.text())
.then((responseText) => {
const id = `price-${this.dataset.section}`;
const html = new DOMParser().parseFromString(responseText, 'text/html')
const destination = document.getElementById(id);
const source = html.getElementById(id);
if (source && destination) destination.innerHTML = source.innerHTML;
document.getElementById(`price-${this.dataset.section}`)?.classList.remove('visibility-hidden');
this.toggleAddButton(!this.currentVariant.available, window.variantStrings.soldOut);
});
}
toggleAddButton(disable = true, text, modifyClass = true) {
const addButton = document.getElementById(`product-form-${this.dataset.section}`)?.querySelector('[name="add"]');
if (!addButton) return;
if (disable) {
addButton.setAttribute('disabled', true);
if (text) addButton.textContent = text;
} else {
addButton.removeAttribute('disabled');
addButton.textContent = window.variantStrings.addToCart;
}
if (!modifyClass) return;
}
setUnavailable() {
const addButton = document.getElementById(`product-form-${this.dataset.section}`)?.querySelector('[name="add"]');
if (!addButton) return;
addButton.textContent = window.variantStrings.unavailable;
document.getElementById(`price-${this.dataset.section}`)?.classList.add('visibility-hidden');
}
getVariantData() {
this.variantData = this.variantData || JSON.parse(this.querySelector('[type="application/json"]').textContent);
return this.variantData;
}
}
customElements.define('variant-selects', VariantSelects);
class VariantRadios extends VariantSelects {
constructor() {
super();
}
updateOptions() {
const fieldsets = Array.from(this.querySelectorAll('fieldset'));
this.options = fieldsets.map((fieldset) => {
return Array.from(fieldset.querySelectorAll('input')).find((radio) => radio.checked).value;
});
}
}
customElements.define('variant-radios', VariantRadios);
Any help or pointers in the right direction would be much appreciated. Cheers
I'm going to use Angular2 to receive websocket incoming messages and update a webpage based on those received messages. Right now, I'm using a dummy echo websocket service and will replace it.
From my understanding, the function which receive websocket messages has to return an observable that is subscribed by a handler who will update the webpage. But I can't figure out how to return an observable.
Code snippet is attached below. The MonitorService creates a websocket connection and return an observable containing the received messages.
#Injectable()
export class MonitorService {
private actionUrl: string;
private headers: Headers;
private websocket: any;
private receivedMsg: any;
constructor(private http: Http, private configuration: AppConfiguration) {
this.actionUrl = configuration.BaseUrl + 'monitor/';
this.headers = new Headers();
this.headers.append('Content-Type', 'application/json');
this.headers.append('Accept', 'application/json');
}
public GetInstanceStatus = (): Observable<Response> => {
this.websocket = new WebSocket("ws://echo.websocket.org/"); //dummy echo websocket service
this.websocket.onopen = (evt) => {
this.websocket.send("Hello World");
};
this.websocket.onmessage = (evt) => {
this.receivedMsg = evt;
};
return new Observable(this.receivedMsg).share();
}
}
Below is another component which subscribes to the observable returned from above and updates webpages correspondingly.
export class InstanceListComponent {
private instanceStatus: boolean
private instanceName: string
private instanceIcon: string
constructor(private monitor: MonitorService) {
this.monitor.GetInstanceStatus().subscribe((result) => {
this.setInstanceProperties(result);
});
}
setInstanceProperties(res:any) {
this.instanceName = res.Instance.toUpperCase();
this.instanceStatus = res.Status;
if (res.Status == true)
{
this.instanceIcon = "images/icon/healthy.svg#Layer_1";
} else {
this.instanceIcon = "images/icon/cancel.svg#cancel";
}
}
}
Now, I'm running into this error in the browser console
TypeError: this._subscribe is not a function
I put it on a plunker and I added a function for sending message to the Websocket endpoint. Here is the important edit:
public GetInstanceStatus(): Observable<any>{
this.websocket = new WebSocket("ws://echo.websocket.org/"); //dummy echo websocket service
this.websocket.onopen = (evt) => {
this.websocket.send("Hello World");
};
return Observable.create(observer=>{
this.websocket.onmessage = (evt) => {
observer.next(evt);
};
})
.share();
}
Update
As you mentioned in your comment, a better alternative way is to use Observable.fromEvent()
websocket = new WebSocket("ws://echo.websocket.org/");
public GetInstanceStatus(): Observable<Event>{
return Observable.fromEvent(this.websocket,'message');
}
plunker example for Observable.fromEvent();
Also, you can do it using WebSocketSubject, although, it doesn't look like it's ready yet (as of rc.4):
constructor(){
this.websocket = WebSocketSubject.create("ws://echo.websocket.org/");
}
public sendMessage(text:string){
let msg = {msg:text};
this.websocket.next(JSON.stringify(msg));
}
plunker example
Get onMessage data from socket.
import { Injectable } from '#angular/core';
import {Observable} from 'rxjs/Rx';
#Injectable()
export class HpmaDashboardService {
private socketUrl: any = 'ws://127.0.0.0/util/test/dataserver/ws';
private websocket: any;
public GetAllInstanceStatus(objStr): Observable<any> {
this.websocket = new WebSocket(this.socketUrl);
this.websocket.onopen = (evt) => {
this.websocket.send(JSON.stringify(objStr));
};
return Observable.create(observer => {
this.websocket.onmessage = (evt) => {
observer.next(evt);
};
}).map(res => res.data).share();
}
**Get only single mesage from socket.**
public GetSingleInstanceStatus(objStr): Observable<any> {
this.websocket = new WebSocket(this.socketUrl);
this.websocket.onopen = (evt) => {
this.websocket.send(JSON.stringify(objStr));
};
return Observable.create(observer => {
this.websocket.onmessage = (evt) => {
observer.next(evt);
this.websocket.close();
};
}).map(res => res.data).share();
}
}
A different approach I used is with subject:
export class WebSocketClient {
private client: WebSocket | undefined;
private subject = new Subject<string>();
...
private connect() {
const client = new WebSocket(fakeUrl);
const client.onmessage = (event) => {
this.subject.next(event.data);
};
}
private watch() { return this.subject } // can be mapped
}
And using it will be in my opinion clearer:
const client = new WebSocketClient(); // can also be injected
client.connect();
client.watch().subscribe(x => ...);
Happy coding!
In my app, i have the regions as header,content,footer - in which on the login page, I don't want to use the header, and footer. for that, on onRender i remove the regions what i don't want to be.
But I am getting an error saying: Cannot read property 'empty' of undefined.
here is my template : (i use jade )
div#wrapper
script(type='text/template', id="appTemplate")
div#header
div#content
div#footer
script(type='text/template', id="loginTemplate")
div this is login template
here is my layout.js:
socialApp.AppLayout = Backbone.Marionette.LayoutView.extend({
el:'#wrapper',
template:'#appTemplate',
regions: {
header : '#header',
content : '#content',
footer : '#footer'
},
onRender : function () {
this.removeRegion("header", "#header"); //i am removing header alone here.
}
});
here is my controller.js
socialApp.loginController = Marionette.Controller.extend({
_initialize:function(){
this.loginView = new loginView({model:new loginModel});
this.layout.onRender(); //calling onRender from here...
this.layout.content.show(this.loginView);
}
});
But it's all not working. any one help me the correct way please?
You should never call methods that are prefixed with on manually. Those are there for your code to react to given events, in this case that the view’s render method was invoked.
I would suggest that you instead of trying to remove and then later re-add regions, you create two different layouts. Then when your router hits the login route, you render LoginLayout into your App’s root region, and for other routes, the ‘normal’ layout. Here’s how I solved something similar:
app.js:
var App = new Marionette.Application;
App.addRegions({ root: '#acme' });
// Instantiate User model
App.addInitializer(function()
{
this.user = new UserModel;
});
// Render App layout
App.addInitializer(function()
{
this.layout = this.user.get('id') ? new ContentLayoutView({ identifier: 'content' }) : new UserLayoutView({ identifier: 'user' });
this.root.show(this.layout);
// And let the routers decide what goes in the content region of each layout
this.router = {
content: new ContentRouter,
user: new UserRouter
};
});
layout/content.js
var ContentLayout = Marionette.LayoutView.extend(
{
identifier: 'content',
template: ContentLayoutTemplate,
regions: {
content: '[data-region="content"]',
panelLeft: '[data-region="panel-left"]',
panelRight: '[data-region="panel-right"]'
},
initialize: function()
{
this.content.once('show', function(view)
{
this.panelLeft.show(new PanelLeftView);
this.panelRight.show(new PanelRightView);
}.bind(this));
}
});
layout/user.js
var UserLayout = Marionette.LayoutView.extend(
{
identifier: 'user',
template: UserLayoutTemplate,
regions: {
content: '[data-region="content"]'
}
});
router/content.js
var ContentRouter = Marionette.AppRouter.extend(
{
routes: {
'(/)': '...'
},
createLayout: function(callback)
{
if(App.root.currentView.options.identifier != 'content')
{
var layout = new ContentLayoutView({ identifier: 'content' });
this.region = layout.content;
this.listenTo(layout, 'show', callback);
App.root.show(layout);
}
else
{
this.region = App.root.currentView.content;
callback();
}
},
execute: function(callback, args)
{
if(App.user.get('id'))
{
this.createLayout(function()
{
callback.apply(this, args);
}.bind(this));
}
else
App.router.user.navigate('login', true);
}
});
router/user.js
var UserRouter = Marionette.AppRouter.extend(
{
routes: {
'login(/)': 'showLogin',
'logout(/)': 'showLogout'
},
createLayout: function(callback)
{
if(App.root.currentView.options.identifier != 'user')
{
var layout = new UserLayoutView({ identifier: 'user' });
this.region = layout.content;
this.listenTo(layout, 'show', callback);
App.root.show(layout);
}
else
{
this.region = App.root.currentView.content;
callback();
}
},
execute: function(callback, args)
{
this.createLayout(function()
{
callback.apply(this, args);
}.bind(this));
},
showLogin: function()
{
var LoginView = require('view/detail/login');
this.region.show(new LoginView);
},
showLogout: function()
{
var LogoutView = require('view/detail/logout');
this.region.show(new LogoutView);
}
});
In an AmpersandJS view, is there a convention for transforming data from a model? For example, I'd like to be able to format a user.joined date within the view before it's displayed. I'd rather not have to do this in the model.
Maybe something like:
var View = require('ampersand-view');
var formatDate = require('../helpers/format-date');
module.exports = View.extend({
bindings: {
'model.joined': {
hook: 'joined',
transform: function(date) { return formatDate(date) }
}
}
});
You are not limited to model when using bindings. You can also bind to properties on the view and since AmpersandView extends AmpersandState these properties may also be derived, like:
var View = require('ampersand-view');
var formatDate = require('../helpers/format-date');
module.exports = View.extend({
derived: {
formattedJoinedDate: {
deps: ['model.joined'],
fn: function() { return formatDate(this.model.joined) }
}
}
bindings: {
'this.formattedJoinedDate': {
hook: 'joined'
}
}
});
Docs that cover bindings to local properties
Don't forget that View extends State, so you can setup properties in your view definition to bind to. Inside your View.extend, you can instantiate things in the props hash, just like you would with ampersand-model or ampersand-state, and then setup bindings on them!
var View = require('ampersand-view');
var formatDate = require('../helpers/format-date');
module.exports = View.extend({
props: { someDate: 'date' },
bindings: {
someDate: {
hook: 'joined',
transform: function(date) { return formatDate(date) }
}
},
initialize: function() { this.someDate = new Date(); }
});