Skip to content

Instantly share code, notes, and snippets.

@machty
Last active October 28, 2018 09:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save machty/985d62c51ff651f65f4aab67ea994eb4 to your computer and use it in GitHub Desktop.
Save machty/985d62c51ff651f65f4aab67ea994eb4 to your computer and use it in GitHub Desktop.
New Twiddle
import Ember from 'ember';
import { lifespan, subscription } from "../subscription";
import { bind } from '@ember/runloop';
import { task, timeout } from 'ember-concurrency';
export default Ember.Controller.extend({
chatRooms:[ "EmberChat", "ReactChat", "VueChat", "AngularChat"],
chatRoom: null,
// chatRoomName: null,
// goal: it should be easy to do `.not()` and tap into preexisting CP macros
chatRoomLifespan: lifespan('chatRoomName', function(chatRoomName) {
return { key: chatRoomName, value: chatRoomName };
}),
chatRoomSubscription: subscription('chatRoomLifespan', function(chatRoomName) {
let intervalId = setInterval(bind(() => {
this.log(`${chatRoomName}: chatRoomSubscription says: ${Math.round(Math.random() * 5000)}`);
}), 500);
return () => clearInterval(intervalId);
}),
chatTask: task(function * (chatRoomName) {
while(true) {
yield timeout(300 + 500 * Math.random())
this.log(`${chatRoomName}: chatTask says: ${Math.round(Math.random() * 5000)}`);
}
}).aliveWithin('chatRoomLifespan'),
messages: [],
log(message) {
let messages = this.messages.slice(0, 20);
messages.unshift(message);
this.set('messages', messages);
},
actions: {
changeChatRoomName(name, e) {
e.preventDefault();
this.set('chatRoom', { name });
}
},
});
import ComputedProperty from '@ember/object/computed';
import { addListener } from '@ember/object/events';
import { addObserver } from '@ember/object/observers';
class SubscriptionManager extends ComputedProperty {
constructor(subscriptionConstructor, dependentKeys) {
function getter(propName) {
let hostObject = this;
let currentSubscriptionKey = `_${propName}`;
let currentSubscription = hostObject[currentSubscriptionKey];
let newValues = dependentKeys.map(key => hostObject[key]);
let newKey = newValues.join(newValues.join(';'));
let currentKey = currentSubscription && currentSubscription.key || "perfect is the enemy of perfectly adequate";
if (newKey === currentKey) {
return currentSubscription.value;
} else if (newKey !== currentKey) {
if (currentSubscription && currentSubscription.dispose) { currentSubscription.dispose(); }
let value = subscriptionConstructor.apply(hostObject, newValues);
let newSubscription;
// it should be possible to return
if (value) {
if (typeof value === 'function') {
newSubscription = { dispose: value, value: true };
} else if (typeof value.dispose === 'function') {
newSubscription = { dispose: () => value.dispose(), value };
} else if (typeof value.cancel === 'function') {
// ember-concurrency hacks: a TaskInstance can be considered a subscription
newSubscription = { dispose: () => value.cancel(), value };
} else {
// TODO: should we complain about a leak? i.e. a subscription with no teardown?
newSubscription = {}
}
} else {
// nothing returned, no new subscription.
newSubscription = {}
}
newSubscription.key = newKey;
if (!currentSubscription) {
_cleanupOnDestroy(hostObject, () => {
let v = hostObject[currentSubscriptionKey];
if (v && v.dispose) {
v.dispose();
v.dispose = null;
}
});
}
hostObject[currentSubscriptionKey] = newSubscription;
return newSubscription.value;
}
}
debugger;
super(getter, { dependentKeys, readOnly: true });
}
setup(proto, propertyName) {
ComputedProperty.prototype.setup.apply(this, arguments);
debugger;
let dks = this._dependentKeys || [];
addListener(proto, "init", null, function() {
// get all dks to ensure observers are active (is this still necessary?)
this.get(propertyName); // kick off initial .get()
});
// the observer keeps the underlying CP alive
addObserver(proto, propertyName, null, function() {
this.get(propertyName);
});
}
}
export function subscription(...args) {
let subscriptionConstructor = args.pop();
let dependentKeys = args;
return new SubscriptionManager(subscriptionConstructor, dependentKeys);
}
// const superSetup = ComputedProperty.prototype.setup;
// SubscriptionManagaer.prototype = Object.create(ComputedProperty.prototype);
// this is lifted from ember-concurrency;
// ember-lifeline does something like this too.
// TL;DR we need a better shared primitive
function _cleanupOnDestroy(owner, object, cleanupMethodName, ...args) {
// TODO: find a non-mutate-y, non-hacky way of doing this.
if (!owner.willDestroy)
{
// we're running in non Ember object (possibly in a test mock)
return;
}
if (!owner.willDestroy.__ember_processes_destroyers__) {
let oldWillDestroy = owner.willDestroy;
let disposers = [];
owner.willDestroy = function() {
for (let i = 0, l = disposers.length; i < l; i ++) {
disposers[i]();
}
oldWillDestroy.apply(owner, arguments);
};
owner.willDestroy.__ember_processes_destroyers__ = disposers;
}
owner.willDestroy.__ember_processes_destroyers__.push(() => {
object[cleanupMethodName](...args);
});
}
<h1>Ember Subscriptions</h1>
<p>
This inspired by / in response to useEffect() hooks unveiled at ReactConf.
The nice thing about the useEffect() hook is that it lets you
co-locate subscription creation with teardown; in the absence
of an API like this, you usually have to put subscription teardown
in a totally separate lifecycle hook, and you often have to stash
some subscription "handle" on the host controller/component
so that the teardown logic in a separate hook can do its job.
</p>
<p>
I've felt and expressed for a while that Ember could benefit
from some stronger primitives for resource ownership and
disposability; hopefully this conveys what I'm going for.
</p>
<p>
One fun thing this demonstrates is how ember-concurrency
task instances implement the "subscription" API :)
</p>
<p>
{{#if chatRoomName}}
Current Chat Room: {{chatRoomName}}
<button onclick={{action 'changeChatRoomName' null}}>Leave</button>
{{else}}
Not in chat room. Please select one below:
{{/if}}
</p>
<ol>
{{#each chatRooms as |chatRoom|}}
<li>
<a href="#" onclick={{action 'changeChatRoomName' chatRoom}}>
{{chatRoom}}
</a>
</li>
{{/each}}
</ol>
<h3>
Messages
{{#if chatRoomName}}
<button onclick={{action 'changeChatRoomName' null}}>Stop</button>
{{/if}}
</h3>
<p>
{{#each messages as |m|}}
{{m}}<br>
{{/each}}
</p>
{
"version": "0.15.1",
"EmberENV": {
"FEATURES": {}
},
"options": {
"use_pods": false,
"enable-testing": false
},
"dependencies": {
"jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.js",
"ember": "3.4.3",
"ember-template-compiler": "3.4.3",
"ember-testing": "3.4.3"
},
"addons": {
"ember-concurrency": "0.8.22"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment