Skip to content

Instantly share code, notes, and snippets.

@machty
Created December 8, 2016 15:24
Show Gist options
  • Save machty/b079238a37af7db788a2862964a2fed8 to your computer and use it in GitHub Desktop.
Save machty/b079238a37af7db788a2862964a2fed8 to your computer and use it in GitHub Desktop.
New Twiddle
import Ember from 'ember';
import { task, timeout } from 'ember-concurrency';
let LOG = [];
export default Ember.Controller.extend({
log: LOG,
usingNested: task(function * () {
log(null, `\nusingNested starting`);
let v = yield using(new Resource("a"), new Resource("b"),
function * (a, b) {
yield timeout(1000);
return 123;
});
log(null, `usingNested: final value ${v}`);
}).restartable(),
usingTop: task(function * () {
log(null, `\nusingTop starting`);
let [a,b] = yield using(new Resource("a"), new Resource("b"));
yield timeout(1000);
}).restartable(),
actions: {
clearLogs() {
LOG.clear();
}
}
});
// TODO: add this as an import
const YIELDABLE = "__ec_yieldable__";
const DISPOSABLE = "__ec_disposable__";
function log(r, msg) {
let prefix = r ? `id=${r.id} name=${r.name}: ` : "";
LOG.pushObject(`${prefix}${msg}`);
}
let resourceId = 1;
function Resource(name) {
this.name = name;
this.id = resourceId++;
log(this, 'initializing');
}
Resource.prototype[DISPOSABLE] = function() {
log(this, 'tearing down');
};
import TaskInstance from 'ember-concurrency/-task-instance';
function registerFinalizers(ti, disposables) {
ti._onFinalize(() => {
disposables.forEach(a => {
if (a && a[DISPOSABLE]) {
a[DISPOSABLE]();
}
});
});
}
function using(...args) {
return {
[YIELDABLE](taskInstance, index) {
let maybeFn = args[args.length-1];
if (typeof maybeFn === 'function') {
debugger;
let ti = TaskInstance.create({
fn: maybeFn,
args,
context: taskInstance.context,
})._start();
registerFinalizers(ti, args);
return ti[YIELDABLE](taskInstance, index);
} else {
registerFinalizers(taskInstance, args);
taskInstance.proceed(index, "next", args);
}
}
};
}
function defer(fn) {
return {
attach(taskInstance, fn) {
taskInstance._onFinalize(fn);
},
[YIELDABLE](taskInstance, index) {
this.attach(taskInstance, fn);
taskInstance.proceed(index, "next", null);
},
};
}
function debounceTask(ms) {
return {
[YIELDABLE](taskInstance, index) {
// TODO: reuse logic from cancelPrevious?
// how should we compose yieldables?
taskInstance.get('task._scheduler.activeTaskInstances').forEach((ti) => {
if (ti !== taskInstance) {
ti.cancel();
}
});
timeout(ms).then(() => {
taskInstance.proceed(index, "next", null);
});
}
};
}
function cancelPrevious() {
return {
[YIELDABLE](taskInstance, index) {
taskInstance.get('task._scheduler.activeTaskInstances').forEach((ti) => {
if (ti !== taskInstance) {
ti.cancel();
}
});
taskInstance.proceed(index, "next", null);
}
};
}
function cancelIfRunning() {
return {
[YIELDABLE](taskInstance, index) {
let found = false;
taskInstance.get('task._scheduler.activeTaskInstances').forEach((ti) => {
if (ti !== taskInstance) {
found = true;
}
});
if (found) {
taskInstance.proceed(index, "cancel", null);
} else {
taskInstance.proceed(index, "next", null);
}
}
};
}
function alert(message) {
return request('dialog', { message });
}
function prompt(message) {
return request('dialog', {
message,
showInput: true,
showCancel: true,
});
}
function confirm(message) {
return request('dialog', {
message,
showCancel: true,
value: true
});
}
// NOTE: everything below here is stuff library/addon authors
// would write. The whole point of the yieldables API is to
// let addon authors experiment with solutions to common
// UI problems involving async.
let INVOKE = "__invoke_symbol__";
let locations = [
'ember-glimmer/helpers/action',
'ember-routing-htmlbars/keywords/closure-action',
'ember-routing/keywords/closure-action'
];
for (let i = 0; i < locations.length; i++) {
if (locations[i] in Ember.__loader.registry) {
INVOKE = Ember.__loader.require(locations[i])['INVOKE'];
break;
}
}
let Channel;
function request(name, payload) {
return {
[YIELDABLE](taskInstance, index) {
let obj = taskInstance.context;
payload.respond = response => {
taskInstance.proceed(index, "next", response);
};
payload.cancel = () => {
taskInstance.proceed(index, "cancel", null);
};
obj.set(name, payload);
return () => {
obj.set(name, null);
};
},
};
}
Channel = Ember.Object.extend({
init() {
this._super();
this._takes = [];
this._puts = [];
this._refreshState();
this.put = (...args) => this._put(...args);
},
isActive: Ember.computed.or('isPutting', 'isTaking'),
isPutting: false,
isTaking: false,
[YIELDABLE](...args) {
// `yield channel` is the same as
// `yield channel.take()`;
return this.take()[YIELDABLE](...args);
},
take() {
let takeAttempt = {
defer: Ember.RSVP.defer(),
active: true,
[YIELDABLE](taskInstance, resumeIndex) {
console.log("take yieldable");
this.defer.promise.then(value => {
console.log(`resolve ${value}`);
taskInstance.proceed(resumeIndex, "next", value);
});
return () => {
this.active = false;
};
},
};
this._takes.push(takeAttempt);
this._scheduleFlush();
return takeAttempt;
},
_put(value) {
let putAttempt = {
value,
defer: Ember.RSVP.defer(),
active: true,
[YIELDABLE](taskInstance, resumeIndex) {
console.log("put yieldable");
this.defer.promise.then(() => {
taskInstance.proceed(resumeIndex, "next", null);
});
return () => {
this.active = false;
};
},
};
this._puts.push(putAttempt);
this._scheduleFlush();
return putAttempt;
},
_scheduleFlush() {
Ember.run.schedule('actions', this, this._flush);
},
_flush() {
let oldTakes = this._takes;
let puts = this._puts;
let newTakes = [];
for (let i = 0; i < oldTakes.length; ++i) {
let take = oldTakes[i];
if (!take.active) { continue; }
while(puts.length) {
let put = puts.shift();
if (!put.active) { continue; }
console.log("resolving take ", take, " with ", put.value);
take.defer.resolve(put.value);
put.defer.resolve();
continue;
}
newTakes.push(take);
}
this._takes = newTakes;
this._refreshState();
},
_refreshState() {
this.setProperties({
isTaking: this._takes.length > 0,
isPutting: this._putting.length > 0,
});
},
});
function channel() {
return Ember.computed(function() {
return Channel.create();
});
}
body {
margin: 12px 16px;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: 12pt;
}
.dialog {
position: fixed;
top: 20%;
left: 50%;
}
.dialog input {
position: relative;
width: 100%;
display: block;
margin-top: 10px;
}
.dialog-inner {
width: 400px;
margin-left: -200px;
background: #d4d0c7;
border-right: 1px solid #404040;
border-bottom: 1px solid #404040;
position: relative;
min-height: 100px;
box-sizing: border-box;
padding: 70px 70px 100px 70px;
}
.dialog-inner::before{
position: absolute;
top: 1px;
left: 1px;
right: 0px;
bottom: 0px;
border-right: 1px solid #808080;
border-bottom: 1px solid #808080;
border-left: 1px solid #ffffff;
border-top: 1px solid #ffffff;
content: ' ';
}
.dialog-header {
position: absolute;
top: 3px;
left: 3px;
right: 3px;
background: blue;
color: white;
padding: 2px 3px;
}
.dialog-message {
text-align: center;
}
.dialog-buttons {
position: absolute;
bottom: 30px;
left: 0;
right: 0;
text-align: center;
}
.dialog-button {
display: inline-block;
border: 1px solid black;
min-width: 100px;
padding: 8px;
text-align: center;
position: relative;
margin: 0 4px;
}
.dialog-button::after {
content: ' ';
position: absolute;
top: 1px;
left: 1px;
right: 0px;
bottom: 0px;
border-top: 1px solid #ffffff;
border-left: 1px solid #ffffff;
border-right: 1px solid #808080;
border-bottom: 1px solid #808080;
}
{{x-description}}
<button onclick={{perform usingNested}}>
usingNested
</button>
<button onclick={{perform usingTop}}>
usingTop
</button>
<button onclick={{action 'clearLogs'}}>
Clear Logs
</button>
<pre>{{#each log as |l|}}{{l}}
{{/each}}</pre>
<h3>C# <code>using</code> statements in Ember</h3>
<h4>(idiomatic resource init/teardown)</h4>
<p>
An Ember implementation of C#'s
<a href="https://msdn.microsoft.com/en-us/library/yh598w02.aspx"><code>using</code></a>
statement, which ensures that some resource
is cleaned up / closed / cancelled at the end of some
block of code. In Ember/JavaScript, this can be useful for
tearing down AJAX/WebSockets or otherwise unsubscribing to
some event source.
</p>
<p>
In this example/experiment, there are two
forms of <code>using</code>:
</p>
<ul>
<li>
nested: disposable args are passed to a nested
task function; when this function completes
(regardless of an exception or no), the args
will be disposed.
</li>
<li>
top-level: the args are registered to be disposed
when the current task returns/completes/errors.
</li>
</ul>
<p>
Cancelling/restarting a task halfway through will
properly teardown the resources (try tapping
the buttons multiple times in a row).
</p>
{
"version": "0.10.1",
"EmberENV": {
"FEATURES": {}
},
"options": {
"use_pods": false,
"enable-testing": false
},
"dependencies": {
"jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.3/jquery.js",
"ember": "2.6.0",
"ember-data": "2.6.1",
"ember-template-compiler": "2.6.0"
},
"addons": {
"ember-concurrency": "pr-100-2"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment