Slide 1

Slide 1 text

Going Realtime With Ember Michael Lange @DingoEatingFuzz

Slide 2

Slide 2 text

s UI Engineer @ HashiCorp 2 Hi, I’m Michael

Slide 3

Slide 3 text

Copyright © 2018 HashiCorp Cloud Infrastructure Automation 3 HashiCorp

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

s Easily Deploy Applications at Any Scale 6 Nomad

Slide 7

Slide 7 text

▪ Lots of stuff going on all the time ▪ Most action is coming from the cluster and users want to stay informed ▪ We can’t get away with waiting for user action Nomad: Takeaway 7

Slide 8

Slide 8 text

One way to do it… 8

Slide 9

Slide 9 text

▪ Twitter tells me about new tweets ▪ My email client pushes notifications ▪ Lyft will show me where my ride is (kinda) ▪ Spotify will tell me what my friends are listening to ▪ Why doesn’t YOUR web app do that? Can’t it just do that automatically? 9

Slide 10

Slide 10 text

s 10 Everything novel and great eventually becomes commodity. Great becomes standard. Standard becomes dated.

Slide 11

Slide 11 text

s 11 Let’s Go Realtime!

Slide 12

Slide 12 text

Copyright © 2018 HashiCorp 12 Time Passes

Slide 13

Slide 13 text

! Warning! This talk doesn’t end with ember install. I’m just gonna talk about stuff. Hopefully that’s still useful?

Slide 14

Slide 14 text

s 14 Step 1 Understand the API Step 2 Break Down the Problem Step 3 Make it Work for One Page Step 4 Find the Right Abstractions Step 5 Celebrate

Slide 15

Slide 15 text

Copyright © 2018 HashiCorp ▪ Long-polling ▪ Using the index query param ▪ Each URL maintains a monotonic index ▪ X-Nomad-Index header in response is the new current index 15 Nomad API

Slide 16

Slide 16 text

Copyright © 2018 HashiCorp ▪ /job/job-1 (current index 10) ▪ Immediately resolve ▪ /job/job-1?index=1 (current index 10) ▪ Immediately resolve ▪ /job/job-1?index=10 (current index 10) ▪ Block until the index increments 16 Nomad API

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

s 18 Well that’s odd…

Slide 19

Slide 19 text

What about WebSockets?

Slide 20

Slide 20 text

What about WebSockets? Have you heard of ServerSentEvents?

Slide 21

Slide 21 text

What about WebSockets? Have you heard of ServerSentEvents? Why not just change the API?

Slide 22

Slide 22 text

▪ APIs can’t always be changed on a whim for the benefit of the UI ▪ This API pre-dates the Web UI ▪ This API works just fine Answer: This API already exists 22

Slide 23

Slide 23 text

▪ Low-tech solution ▪ It’s entirely stateless ▪ It’s naturally fault-tolerant Answer: This API is actually good 23

Slide 24

Slide 24 text

s 24 Step 1 Understand the API Step 2 Break Down the Problem Step 3 Make it Work for One Page Step 4 Find the Right Abstractions Step 5 Celebrate

Slide 25

Slide 25 text

1. Implement long-polling 2. Update pages to use long-polling 3. Re-render all the things when long-polls resolve with new data Breaking down the problem 25

Slide 26

Slide 26 text

26 Long polling in Bash # Starting index idx="1" while true do # Append index as a query param url="http://localhost:4646/v1/job/example?index=$idx" echo -e "\nCurling $url..." curl -i -s $url | grep -v "^" > polltmp while read -r line do # Use the X-Nomad-Index header to set the new index key="$(echo $line | cut -d ':' -f 1)" val="$(echo $line | cut -d ':' -f 2 | tr -d '[:space:]')" if [ "$key" = "X-Nomad-Index" ] then idx="$val" fi done < polltmp # Do stuff with the payload tail -1 polltmp | jq ". | {Name, Status, ModifyIndex}" done simplepoll.sh

Slide 27

Slide 27 text

27 Long polling in Bash # Starting index idx="1" while true do # Append index as a query param url="http://localhost:4646/v1/job/example?index=$idx" echo -e "\nCurling $url..." curl -i -s $url | grep -v "^" > polltmp while read -r line do # Use the X-Nomad-Index header to set the new index key="$(echo $line | cut -d ':' -f 1)" val="$(echo $line | cut -d ':' -f 2 | tr -d '[:space:]')" if [ "$key" = "X-Nomad-Index" ] then idx="$val" fi done < polltmp # Do stuff with the payload tail -1 polltmp | jq ". | {Name, Status, ModifyIndex}" done simplepoll.sh

Slide 28

Slide 28 text

28 Long polling in Bash # Starting index idx="1" while true do # Append index as a query param url="http://localhost:4646/v1/job/example?index=$idx" echo -e "\nCurling $url..." curl -i -s $url | grep -v "^" > polltmp while read -r line do # Use the X-Nomad-Index header to set the new index key="$(echo $line | cut -d ':' -f 1)" val="$(echo $line | cut -d ':' -f 2 | tr -d '[:space:]')" if [ "$key" = "X-Nomad-Index" ] then idx="$val" fi done < polltmp # Do stuff with the payload tail -1 polltmp | jq ". | {Name, Status, ModifyIndex}" done simplepoll.sh

Slide 29

Slide 29 text

29 Long polling in Bash # Starting index idx="1" while true do # Append index as a query param url="http://localhost:4646/v1/job/example?index=$idx" echo -e "\nCurling $url..." curl -i -s $url | grep -v "^" > polltmp while read -r line do # Use the X-Nomad-Index header to set the new index key="$(echo $line | cut -d ':' -f 1)" val="$(echo $line | cut -d ':' -f 2 | tr -d '[:space:]')" if [ "$key" = "X-Nomad-Index" ] then idx="$val" fi done < polltmp # Do stuff with the payload tail -1 polltmp | jq ". | {Name, Status, ModifyIndex}" done simplepoll.sh

Slide 30

Slide 30 text

30 Long polling in Bash # Starting index idx="1" while true do # Append index as a query param url="http://localhost:4646/v1/job/example?index=$idx" echo -e "\nCurling $url..." curl -i -s $url | grep -v "^" > polltmp while read -r line do # Use the X-Nomad-Index header to set the new index key="$(echo $line | cut -d ':' -f 1)" val="$(echo $line | cut -d ':' -f 2 | tr -d '[:space:]')" if [ "$key" = "X-Nomad-Index" ] then idx="$val" fi done < polltmp # Do stuff with the payload tail -1 polltmp | jq ". | {Name, Status, ModifyIndex}" done simplepoll.sh

Slide 31

Slide 31 text

Make it work Make it nice Make it fast

Slide 32

Slide 32 text

Make it work Make it nice Make it fast

Slide 33

Slide 33 text

No content

Slide 34

Slide 34 text

It me!

Slide 35

Slide 35 text

Bryan Berg, https://www.cardstacker.com/#/tallest-house-of-cards/

Slide 36

Slide 36 text

▪ Inaccurate project estimates ▪ Fear of certain files or subsystems ▪ Habitual refactoring ▪ Unhappy developers ▪ Unconfident developers Symptoms include… 36

Slide 37

Slide 37 text

How do we manage complexity?

Slide 38

Slide 38 text

✨ Abstractions! ✨

Slide 39

Slide 39 text

Minerva in Her Study 1635, Rembrandt van Rijn https://www.theleidencollection.com/artwork/minerva-in-her-study/

Slide 40

Slide 40 text

Smiley in Open Sans 2018, Michael Lange Minerva in Her Study 1635, Rembrandt van Rijn https://www.theleidencollection.com/artwork/minerva-in-her-study/

Slide 41

Slide 41 text

Smiley in Open Sans 2018, Michael Lange Minerva in Her Study 1635, Rembrandt van Rijn https://www.theleidencollection.com/artwork/minerva-in-her-study/ 100% Abstract 0% Abstract

Slide 42

Slide 42 text

There is no one true Goldilocks level of abstraction.

Slide 43

Slide 43 text

There is no one true Goldilocks level of abstraction. It’s situational

Slide 44

Slide 44 text

Make it work Make it nice Make it fast

Slide 45

Slide 45 text

Make it work Make it nice Make it fast

Slide 46

Slide 46 text

s 46 Step 1 Understand the API Step 2 Break Down the Problem Step 3 Make it Work for One Page Step 4 Find the Right Abstractions Step 5 Celebrate

Slide 47

Slide 47 text

▪ Adapters ▪ Index query param ▪ Conditionally use the query param (allow non-blocking requests) ▪ For every adapter that supports blocking queries ▪ Serializers ▪ Nothing ▪ Models ▪ Nothing Changes to the data layer 47

Slide 48

Slide 48 text

48 Changes to the data layer this.get('store').findRecord('job', id, { reload: true, adapterOptions: { watch: true }, }); route.js

Slide 49

Slide 49 text

49 Changes to the data layer findRecord(store, type, id, snapshot) { if (get(snapshot || {}, 'adapterOptions.watch')) { // do some stuff for blocking queries } return this._super(...arguments); }, watchable.js

Slide 50

Slide 50 text

▪ How do we track the X-Nomad-Index value? ▪ WatchList service Changes to the data layer 50

Slide 51

Slide 51 text

No content

Slide 52

Slide 52 text

No content

Slide 53

Slide 53 text

53 Changes to the data layer watchList: service(), // ... findRecord(store, type, id, snapshot) { const fullUrl = this.buildURL(type.modelName, id, snapshot, 'findRecord'); let [url, params] = fullUrl.split('?'); params = assign(queryString.parse(params) || {}, this.buildQuery()); if (get(snapshot || {}, 'adapterOptions.watch')) { params.index = this.get('watchList').getIndexFor(url); } return this.ajax(url, 'GET', { data: params, }); }, watchable.js

Slide 54

Slide 54 text

54 Changes to the data layer handleResponse(status, headers, payload, requestData) { // Some browsers lowercase all headers. Others keep them // case sensitive. const newIndex = headers['x-nomad-index'] || headers['X-Nomad-Index']; if (newIndex) { this.get('watchList').setIndexFor(requestData.url, newIndex); } return this._super(...arguments); }, watchable.js

Slide 55

Slide 55 text

55 Just an object with write protections. Changes to the data layer import { readOnly } from '@ember/object/computed'; import { copy } from '@ember/object/internals'; import Service from '@ember/service'; let list = {}; export default Service.extend({ list: readOnly(function() { return copy(list, true); }), init() { list = {}; }, getIndexFor(url) { return list[url] || 1; }, setIndexFor(url, value) { list[url] = +value; }, }); services/watch-list.js

Slide 56

Slide 56 text

▪ Model hook stays the same ▪ Immediately get the current state of the model in question ▪ Need some sort of polling mechanism to continuously fetch the model using our new adapterOption Changes to the route 56

Slide 57

Slide 57 text

▪ Asynchronous loops look like synchronous ones ▪ Tasks are easily canceled Ember Concurrency! 57

Slide 58

Slide 58 text

58 Polling loop import Route from '@ember/routing/route'; import { task, timeout } from 'ember-concurrency'; export default Route.extend({ setupController(controller, model) { this.get('watch').perform(model); return this._super(...arguments); }, watch: task(function*(model) { while (!Ember.testing) { try { yield this.get('store').findRecord('job', model.get('id'), { reload: true, adapterOptions: { watch: true }, }); yield timeout(2000); } catch (e) { yield e; } } }), }); routes/jobs/job/index.js

Slide 59

Slide 59 text

59 Polling loop import Route from '@ember/routing/route'; import { task, timeout } from 'ember-concurrency'; export default Route.extend({ setupController(controller, model) { this.get('watch').perform(model); return this._super(...arguments); }, watch: task(function*(model) { while (!Ember.testing) { try { yield this.get('store').findRecord('job', model.get('id'), { reload: true, adapterOptions: { watch: true }, }); yield timeout(2000); } catch (e) { yield e; } } }), }); routes/jobs/job/index.js

Slide 60

Slide 60 text

▪ Adapter changes ▪ New service ▪ Route changes So that’s it? 60

Slide 61

Slide 61 text

▪ Watching lists ▪ Watching relationships ▪ Removing things from the store Not quite 61

Slide 62

Slide 62 text

▪ Override findAll ▪ Use existing index tracking service Watching lists 62

Slide 63

Slide 63 text

63 Watching lists watchList: service(), // ... findAll(store, type, sinceToken, snapshotRecordArray) { const params = this.buildQuery(); const url = this.urlForFindAll(type.modelName); if (get(snapshotRecordArray || {}, 'adapterOptions.watch')) { params.index = this.get('watchList').getIndexFor(url); } return this.ajax(url, 'GET', { data: params, }); }, watchable.js

Slide 64

Slide 64 text

▪ No existing store method for fetching a relationship ▪ It’s done by reading properties on models ▪ Solution: a new reloadRelationship method Watching relationships 64

Slide 65

Slide 65 text

65 Models have APIs for looking up details for a relationship Watching relationships reloadRelationship(model, relationshipName, watch = false) { const relationship = model.relationshipFor(relationshipName); if (relationship.kind !== 'belongsTo' && relationship.kind !== 'hasMany') { throw new Error( `${relationship.key} must be a belongsTo or hasMany, instead it was ${relationship.kind}` ); } else { const url = model[relationship.kind](relationship.key).link(); let params = {}; if (watch) { params.index = this.get('watchList').getIndexFor(url); } // Avoid duplicating existing query params by passing them to ajax // in the URL and in options.data if (url.includes('?')) { const paramsInUrl = queryString.parse(url.split('?')[1]); Object.keys(paramsInUrl).forEach(key => { delete params[key]; }); } return this.ajax(url, 'GET', { data: params, }).then( json => { const store = this.get('store'); const normalizeMethod = relationship.kind === 'belongsTo' ? 'normalizeFindBelongsToResponse' : 'normalizeFindHasManyResponse'; const serializer = store.serializerFor(relationship.type); const modelClass = store.modelFor(relationship.type); const normalizedData = serializer[normalizeMethod](store, modelClass, json); store.push(normalizedData); }, error => { if (error instanceof AbortError) { return relationship.kind === 'belongsTo' ? {} : []; } throw error; } ); } }, watchable.js

Slide 66

Slide 66 text

▪ findAll case ▪ Remove records from the store that aren’t also in the new payload. ▪ watchRelationship case ▪ Remove records from the store of the relationship type that aren’t also in this payload. Removing records 66

Slide 67

Slide 67 text

No content

Slide 68

Slide 68 text

68 By default, adds and removes relationship links related to this model Updating hasMany findHasMany(store, snapshot, link, relationship) { return this._super(...arguments); } adapters/application.js

Slide 69

Slide 69 text

No content

Slide 70

Slide 70 text

70 By using the relationship traversal APIs we can remove the record on the other side of the relationship Removing related records findHasMany(store, snapshot, link, relationship) { return this._super(...arguments).then(payload => { const relationshipType = relationship.type; const inverse = snapshot.record.inverseFor(relationship.key); if (inverse) { store .peekAll(relationshipType) .filter(record => record.get(`${inverse.name.id}`) === snapshot.id) .forEach(record => { record.unloadRecord(); }); } return payload; }); } adapters/application.js

Slide 71

Slide 71 text

No content

Slide 72

Slide 72 text

72 By using store.push and JSONAPI, we can also clear the relationships on the related model we’re unloading Clearing removed record relationships export default function removeRecord(store, record) { const relationshipMeta = []; record.eachRelationship((key, { kind }) => { relationshipMeta.push({ key, kind }); }); store.push({ data: { id: record.get('id'), type: record.constructor.modelName, relationships: relationshipMeta.reduce((hash, rel) => { hash[rel.key] = { data: rel.kind === 'hasMany' ? [] : null }; return hash; }, {}), }, }); record.unloadRecord(); } // Sample JSONAPI payload provided to store.push // { // id: “alloc-1”, // type: “allocation”, // relationships: { // job: null, // node: null, // evaluations: [], // } // } utils/remove-record.js

Slide 73

Slide 73 text

No content

Slide 74

Slide 74 text

No content

Slide 75

Slide 75 text

▪ New service for tracking indices ▪ findRecord override ▪ findAll override ▪ New reloadRelationship ▪ Handle removing data from the store Now it works 75

Slide 76

Slide 76 text

s 76 Step 1 Understand the API Step 2 Break Down the Problem Step 3 Make it Work for One Page Step 4 Find the Right Abstractions Step 5 Celebrate

Slide 77

Slide 77 text

Developer Concerns Abstracted Details Requesting models Index tracking Deciding what data to poll Removing stale data Deciding how to poll Deciding when to poll

Slide 78

Slide 78 text

Developer Concerns Abstracted Details Requesting models Index tracking Deciding what data to poll Removing stale data Deciding how to poll Deciding when to poll

Slide 79

Slide 79 text

Developer Concerns Abstracted Details Requesting models Index tracking Deciding what data to poll Deciding how to poll Deciding when to poll Removing stale data

Slide 80

Slide 80 text

Developer Concerns Abstracted Details Requesting models Index tracking Removing stale data Deciding what data to poll Deciding how to poll Deciding when to poll

Slide 81

Slide 81 text

Developer Concerns Abstracted Details Requesting models Index tracking Deciding what data to poll Removing stale data Deciding how to poll Deciding when to poll

Slide 82

Slide 82 text

▪ Already using Ember Concurrency tasks ▪ Computed property macros ▪ What are truly parameters and not the mechanics of polling? Abstracting How to Poll 82

Slide 83

Slide 83 text

83 What are truly parameters and not the mechanics of polling? Task Macro utils/properties/watch.js import Ember from 'ember'; import { get } from '@ember/object'; import RSVP from 'rsvp'; import { task, timeout } from 'ember-concurrency'; export function watchRecord(modelName) { return task(function*(id, throttle = 2000) { if (typeof id === 'object') { id = get(id, 'id'); } while (!Ember.testing) { try { yield this.get('store').findRecord(modelName, id, { reload: true, adapterOptions: { watch: true }, }); yield timeout(throttle); } catch (e) { yield e; } } }).drop(); }

Slide 84

Slide 84 text

▪ Starting tasks in setupController ▪ Move that into a Mixin Abstracting When to Poll 84

Slide 85

Slide 85 text

85 Remove the responsibility of choosing when to start polls from routes. Mixin import Mixin from '@ember/object/mixin'; import { computed } from '@ember/object'; import { assert } from '@ember/debug'; export default Mixin.create({ watchers: computed(() => []), cancelAllWatchers() { this.get('watchers').forEach(watcher => { assert( 'Watchers must be Ember Concurrency Tasks.', !!watcher.cancelAll ); watcher.cancelAll(); }); }, startWatchers() { assert('startWatchers needs to be overridden in the Route', false); }, setupController() { this.startWatchers(...arguments); return this._super(...arguments); }, actions: { willTransition() { this.cancelAllWatchers(); }, }, }); mixin/with-watchers.js

Slide 86

Slide 86 text

86 So clean! import Route from '@ember/routing/route'; import { collect } from '@ember/object/computed'; import { watchAll } from 'nomad-ui/utils/properties/watch'; import WithWatchers from 'nomad-ui/mixins/with-watchers'; export default Route.extend(WithWatchers, { startWatchers(controller) { controller.set('modelWatch', this.get('watch').perform()); }, watch: watchAll('job'), watchers: collect('watch'), }); routes/jobs/index.js

Slide 87

Slide 87 text

87 Some nuance is still necessary. Route-specific behavior import Route from '@ember/routing/route'; import { collect } from '@ember/object/computed'; import { watchRecord, watchRelationship, watchAll } from 'nomad-ui/utils/properties/watch'; import WithWatchers from 'nomad-ui/mixins/with-watchers'; export default Route.extend(WithWatchers, { startWatchers(controller, model) { if (!model) { return; } controller.set('watchers', { model: this.get('watch').perform(model), summary: this.get('watchSummary').perform(model.get('summary')), allocations: this.get('watchAllocations').perform(model), evaluations: this.get('watchEvaluations').perform(model), latestDeployment: model.get('supportsDeployments') && this.get('watchLatestDeployment').perform(model), list: model.get('hasChildren') && this.get('watchAll').perform(), }); }, watch: watchRecord('job'), watchAll: watchAll('job'), watchSummary: watchRecord('job-summary'), watchAllocations: watchRelationship('allocations'), watchEvaluations: watchRelationship('evaluations'), watchLatestDeployment: watchRelationship('latestDeployment'), watchers: collect( 'watch', 'watchAll', 'watchSummary', 'watchAllocations', 'watchEvaluations', 'watchLatestDeployment' ), }); routes/jobs/job/index.js

Slide 88

Slide 88 text

Copyright © 2018 HashiCorp 88 Time Passes

Slide 89

Slide 89 text

Video of bug from not canceling requests

Slide 90

Slide 90 text

s 90 Step 1 Understand the API Step 2 Break Down the Problem Step 3 Make it Work for One Page Step 4 Find the Right Abstractions Step 5 Step 6 Celebrate Step 5 Fight the Bugs

Slide 91

Slide 91 text

▪ Has nothing to do with Ember ▪ Has nothing to do with the code we just wrote ▪ Has everything to do with browsers having a max number of connections per domain Requests are stuck pending, what gives? 91

Slide 92

Slide 92 text

s 92 No matter what framework you choose, eventually browsers will be your problem.

Slide 93

Slide 93 text

No content

Slide 94

Slide 94 text

s 94 Most people are making the best decisions they can in the situations they are in.

Slide 95

Slide 95 text

HTTP/1.1 RFC 2616 95 A single-user client SHOULD NOT maintain more than 2 connections with any server or proxy. … These guidelines are intended to improve HTTP response times and avoid congestion. — https://tools.ietf.org/html/rfc2616#page-46

Slide 96

Slide 96 text

▪ Better than HTTP/1.1 in every single way ▪ Literally magic Solution 1: HTTP/2 96

Slide 97

Slide 97 text

Copyright © 2018 HashiCorp ▪ Browsers will only use HTTP/2 over a secure connection ▪ Nomad supports TLS, but it is optional 97 Why we can’t use HTTP/2

Slide 98

Slide 98 text

▪ Use multiple sub-domains to bypass the max connections per domain limit. ▪ Doesn’t even matter if all you’re doing is creating a reverse proxy to the same exact server Solution 2: Domain Sharding 98

Slide 99

Slide 99 text

Copyright © 2018 HashiCorp ▪ Not SaaS! ▪ Customers install Nomad on their own machines ▪ We take operational simplicity very seriously 99 Why we can’t use Domain Sharding

Slide 100

Slide 100 text

s The first step is DNS 100 Using the Web UI

Slide 101

Slide 101 text

▪ HTTP requests can be canceled ▪ XMLHttpRequest objects have an abort method for triggering cancellation ▪ Ember Data uses XMLHttpRequests ▪ However, Ember Data does not provide abort hooks Solution 3: Request Cancellation 101

Slide 102

Slide 102 text

▪ Capture XHRs in some sort of cache to abort later ▪ Remove XHRs from the cache when they resolve ▪ Write cancel methods that mirror the watchable method signatures ▪ Call the cancel methods in our polling code Doing Request Cancellation with Ember Data Anyway 102

Slide 103

Slide 103 text

103 A registry mapping keys to queues of XHRs. Capture all the XHRs xhrs: computed(function() { return { list: {}, track(key, xhr) { if (this.list[key]) { this.list[key].push(xhr); } else { this.list[key] = [xhr]; } }, cancel(key) { while (this.list[key] && this.list[key].length) { this.remove(key, this.list[key][0]); } }, remove(key, xhr) { if (this.list[key]) { xhr.abort(); this.list[key].removeObject(xhr); } }, }; }), watchable.js

Slide 104

Slide 104 text

104 ajaxOptions provides an opportunity to capture an XHR to put in our registry. Capture all the XHRs ajaxOptions() { const ajaxOptions = this._super(...arguments); const key = this.xhrKey(...arguments); const previousBeforeSend = ajaxOptions.beforeSend; ajaxOptions.beforeSend = function(jqXHR) { if (previousBeforeSend) { previousBeforeSend(...arguments); } this.get('xhrs').track(key, jqXHR); jqXHR.always(() => { this.get('xhrs').remove(key, jqXHR); }); }; return ajaxOptions; }, watchable.js

Slide 105

Slide 105 text

105 In order to control cancellation from outside adapters. Cancel Methods cancelFindRecord(modelName, id) { if (!modelName || id == null) { return; } const url = this.urlForFindRecord(id, modelName); this.get('xhrs').cancel(`GET ${url}`); }, cancelFindAll(modelName) { if (!modelName) { return; } let url = this.urlForFindAll(modelName); const params = queryString.stringify(this.buildQuery()); if (params) { url = `${url}?${params}`; } this.get('xhrs').cancel(`GET ${url}`); }, cancelReloadRelationship(model, relationshipName) { if (!model || !relationshipName) { return; } const relationship = model.relationshipFor(relationshipName); if (relationship.kind !== 'belongsTo' && relationship.kind !== 'hasMany') { throw new Error( `${relationship.key} must be a belongsTo or hasMany, instead it was ${relationship.kind}` ); } else { const url = model[relationship.kind](relationship.key).link(); this.get('xhrs').cancel(`GET ${url}`); } }, watchable.js

Slide 106

Slide 106 text

106 Stopping polling should result in canceling requests. Call Cancel Methods export function watchRecord(modelName) { return task(function*(id, throttle = 2000) { if (typeof id === 'object') { id = get(id, 'id'); } while (!Ember.testing) { try { yield this.get('store').findRecord(modelName, id, { reload: true, adapterOptions: { watch: true }, }); yield timeout(throttle); } catch (e) { yield e; } finally { this.get('store') .adapterFor(modelName) .cancelFindRecord(modelName, id); } } }).drop(); } watchable.js

Slide 107

Slide 107 text

▪ An Aborted XHR results in an AbortError being thrown ▪ We don’t want an error thrown, we expect an abort! ▪ Handle the AbortError case in the adapter find methods Unforeseen Consequences: Error Handling 107

Slide 108

Slide 108 text

108 A downside of promise- based control-flow. Handle expected errors import { AbortError } from 'ember-data/adapters/errors'; // ... findRecord(store, type, id, snapshot) { const fullUrl = this.buildURL(type.modelName, id, snapshot, 'findRecord') let [url, params] = fullUrl.split('?'); params = assign(queryString.parse(params) || {}, this.buildQuery()); if (get(snapshot || {}, 'adapterOptions.watch')) { params.index = this.get('watchList').getIndexFor(url); } return this.ajax(url, 'GET', { data: params, }).catch(error => { if (error instanceof AbortError) { return; } throw error; }); }, watchable.js

Slide 109

Slide 109 text

s 109 Step 1 Understand the API Step 2 Break Down the Problem Step 3 Make it Work for One Page Step 4 Find the Right Abstractions Step 5 Step 6 Celebrate Step 5 Fight the Bugs

Slide 110

Slide 110 text

Look back on what we built 110 New service to manage indices New methods for reloading data and canceling requests New methods for removing data from the store Reusable patterns for adding realtime behaviors to a page Polling code to continuously long-poll and stop on demand.

Slide 111

Slide 111 text

The code we didn’t write 111 An architecture for maintaining an interface with a backend A way to make any data consistent from within the app State mgmt for records A well-rounded modern approach to asynchronous control-flow An object and inheritance model that makes it easy to build abstractions

Slide 112

Slide 112 text

The code we didn’t write 112 KVO Two-way data-binding DDAU Speedy re-renders

Slide 113

Slide 113 text

Distribution of code that gets run 113 Ember Ember Data Addons Nomad UI This is totally unscientific, just trying to drive a point home

Slide 114

Slide 114 text

▪ Products are all trying to be unique and differentiated ▪ By definition frameworks only solve common problems ▪ We can use addons and build on top of Ember to create great new things ▪ We can think like framework authors to build software that might withstand the test of time and keep our coworkers happy. The Takeaways 114

Slide 115

Slide 115 text

One more thing.

Slide 116

Slide 116 text

The Nomad UI is Open Source! https://github.com/hashicorp/nomad/pull/3936

Slide 117

Slide 117 text

HashiCorp ❤ Open Source & Ember.js 117 Consul + Consul UI https://github.com/hashicorp/consul Vault + Vault UI https://github.com/hashicorp/vault Nomad + Nomad UI https://github.com/hashicorp/nomad Terraform Enterprise Not open source, but still Ember!

Slide 118

Slide 118 text

Thank you. www.hashicorp.com Michael Lange @DingoEatingFuzz