Skip to content

Instantly share code, notes, and snippets.

@Bestra
Last active November 6, 2024 01:10
Show Gist options
  • Save Bestra/9793cb1b7f5f4e16935f to your computer and use it in GitHub Desktop.
Save Bestra/9793cb1b7f5f4e16935f to your computer and use it in GitHub Desktop.
Cat Trader
import Ember from 'ember';
export default Ember.Controller.extend({
websocket: Ember.inject.service(),
listener: Ember.inject.service('websocket-listener'),
appName:'Cat Trader',
users: Ember.computed.alias('model'),
payloads: [],
init() {
this._super(...arguments);
let listener = this.get('listener');
listener.set('messageCallback', this.showPayloads.bind(this));
listener.listen();
},
showPayloads(m) {
this.get('payloads').pushObject(m);
},
actions: {
trade(currentUserIndex, cat, inc) {
let newUser = this.get('users').objectAt(currentUserIndex + inc);
if (!newUser) { return; };
cat.set('user', newUser);
},
rename(cat, newName) {
cat.set('name', newName);
},
fakeWebsocket(fnName) {
this.get('websocket')[fnName]();
}
}
});
import Ember from 'ember';
import Factory from 'demo-app/factories/payload'
const catPayload = {
data: [
Factory.user("1", "Batman", ["1"]),
Factory.user("2", "Robin", ["2"])
],
included: [
Factory.cat("1", "Fluffy", "1"),
Factory.cat("2", "Cuddles", "2")
]
}
export default Ember.Route.extend({
beforeModel() {
this.store.push(catPayload);
},
model() {
return this.store.peekAll('user');
},
setupController(controller, model) {
controller.set('model', model);
controller.set('catCount', model.get('length'));
}
});
<h1>Welcome to {{appName}}</h1>
<h4>Instructions</h4>
<p>
This is a little example of what could go awry if a model is updated from the store
while it has unsaved changes on the client. A user hasMany cats, and a cat belongsTo a user. Trading a cat from one user to another changes the relationships on the models locally, but those changes can conflict if new information comes down from the server.
</p>
<p> To see that unsaved changes to attributes take precedence over server-sent updates:</p>
<ol>
<li>Use the 'Rename' button and change Fluffy's name</li>
<li>Rename Fluffy via a websocket update</li>
</ol>
<p>
The unsaved name sticks. A change pushed into the store won't override a dirty attribute.
</p>
<br>
<p> In contrast, try the following:</p>
<ol>
<li>Trade Fluffy down to Robin</li>
<li>Add Patches to Batman via a websocket update</li>
</ol>
<p>
You'll see that Fluffy belongs to both Batman and Robin. Since the cat was traded but not saved the server
could plausibly send an update that includes conflicting relationship information.
</p>
<div>
{{#each model as |user index|}}
<h3>{{user.name}}'s {{user.cats.length}} Cats</h3>
<hr>
<ul>
{{#each user.cats as |cat|}}
{{cat-item cat=cat trade=(action 'trade' index cat)
rename=(action 'rename' cat)}}
{{/each}}
</ul>
{{/each}}
</div>
{{outlet}}
<br>
<br>
<h3>Push the buttons below to send websocket updates</h3>
<button {{action 'fakeWebsocket' 'addPatches'}}>Add Patches to Batman</button>
<button {{action 'fakeWebsocket' 'renameFluffy'}}>Rename Fluffy to 'Steve'</button>
<button {{action 'fakeWebsocket' 'deleteCuddles'}}>Delete Cuddles</button>
<div>
<p>Recieved Websocket payloads:</p>
{{#each payloads as |payload|}}
<pre>{{payload}}</pre>
<hr>
{{/each}}
</div>
import Ember from 'ember';
export default Ember.Component.extend({
cat: null,
newName: "",
actions: {
startRename() {
this.setProperties({renaming: true, newName: this.get('cat.name')});
},
confirmRename() {
this.set('renaming', false);
this.attrs.rename(this.get('newName'));
}
}
});
{{#if renaming}}
{{input value=newName}}
<button {{action "confirmRename"}}> Confirm</button>
{{else}}
{{cat.name}} belongs to {{cat.user.name}}
<button {{action "startRename"}}> Rename</button>
{{/if}}
<button {{action attrs.trade -1}}> Trade Up</button>
<button {{action attrs.trade 1}}> Trade Down</button>
Title: Bad Stuff
Client->Server:Save Cat 3
Websocket->Client: Cat 3 Updated
Note over Client: WTF?
Server->Client: Cat 3 Saved
let user = function(id, name, catIds) {
let cats = catIds.map(function(i) {
return {type: "cat", id: id};
});
return {
type: "user",
id: id,
attributes: {name: name},
relationships: {
cats: {
data: cats
}
}
};
}
let cat = function(id, name, userId) {
return {
type: "cat",
id: id,
attributes: {name: name},
relationships: {
user: {
data: {type: "user", id: userId}
}
}
};
}
export default {user, cat};
import DS from 'ember-data';
export default DS.Model.extend({
name: DS.attr('string'),
user: DS.belongsTo('user', {async: false})
});
import DS from 'ember-data';
export default DS.Model.extend({
name: DS.attr('string'),
cats: DS.hasMany('cat', {async: false})
});
import Ember from 'ember';
import config from './config/environment';
const Router = Ember.Router.extend({
location: config.locationType
});
Router.map(function() {
});
export default Router;
{
"version": "0.5.0",
"EmberENV": {
"FEATURES": {}
},
"options": {
"enable-testing": false
},
"dependencies": {
"jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.3/jquery.js",
"ember": "https://cdnjs.cloudflare.com/ajax/libs/ember.js/2.2.0/ember.debug.js",
"ember-data": "https://cdnjs.cloudflare.com/ajax/libs/ember-data.js/2.2.1/ember-data.js",
"ember-template-compiler": "https://cdnjs.cloudflare.com/ajax/libs/ember.js/2.2.0/ember-template-compiler.js",
"jquery-mockjax": "https://cdnjs.cloudflare.com/ajax/libs/jquery-mockjax/1.6.2/jquery.mockjax.js"
}
}
import Ember from 'ember';
export default Ember.Service.extend({
websocket: Ember.inject.service(),
store: Ember.inject.service(),
messageCallback: null,
init() {
this._super(...arguments);
},
handleMessage(m) {
if (this.messageCallback) {
this.messageCallback(m);
}
let { type, data } = JSON.parse(m);
if (type === 'update') {
this.update(data);
} else if (type === 'delete') {
this.delete(data);
}
},
update(data) {
console.log(`pushing ${data}`);
this.get('store').push({data});
},
delete({type, id}) {
let record = this.get('store').peekRecord(type, id);
console.log(`deleting ${record}`);
if (record) { this.get('store').unloadRecord(record); }
},
listen() {
let fn = this.handleMessage.bind(this);
this.get('websocket').on('message', fn);
}
});
import Ember from 'ember';
import Factory from 'demo-app/factories/payload'
export default Ember.Service.extend(Ember.Evented, {
addPatches() {
let payload = {
type: 'update',
data: Factory.cat("3", "Patches", "1")
}
this.trigger('message', JSON.stringify(payload, null, 2));
},
renameFluffy() {
let payload = {
type: 'update',
data: Factory.cat("1", "Steve", "1")
}
this.trigger('message', JSON.stringify(payload, null, 2));
},
deleteCuddles() {
let payload = {
type: 'delete',
data: {type: "cat", id: "2"}
}
this.trigger('message', JSON.stringify(payload, null, 2));
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment