Skip to content

Instantly share code, notes, and snippets.

@lifeart
Last active November 27, 2021 10:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save lifeart/9c81213d298d06a4514a23dbdcb8813a to your computer and use it in GitHub Desktop.
Save lifeart/9c81213d298d06a4514a23dbdcb8813a to your computer and use it in GitHub Desktop.
Tracked Observers
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { getOwner } from '@ember/application';
import { cached } from 'tracked-toolbox';
import { scheduleOnce } from '@ember/runloop';
const Observers = [];
class BasicObserver {
tags = [];
cb = null;
name = '(unknown observer)';
constructor(tags, cb, name) {
this.tags = tags();
this.cb = cb;
this.name = name;
}
recompute() {
return this.value;
}
isDestroyed = false;
runCb() {
return this.cb();
}
@cached
get value() {
if (this.isDestroyed) {
return;
}
let result = this.runCb();
if (typeof result === 'object' && Array.isArray(result)) {
this.tags = result;
} else if (typeof result === 'function') {
this.tags = [result];
} else if (typeof result === 'object' && result !== null && ('then' in result)) {
throw new Error('async effects is not supported');
}
this.tags.forEach(t => t());
return;
}
destroy() {
this.isDestroyed = true;
this.cb = null;
this.tags = [];
}
}
class SafeObserver extends BasicObserver {
timeout = null;
runCb() {
clearTimeout(this.timeout);
this.timeout = setTimeout(() => this.cb());
}
destroy() {
clearTimeout(this.timeout);
super.destroy();
}
}
function createObserverInstance(fn) {
const instance = fn();
Observers.push(instance);
return function () {
Observers.splice(Observers.indexOf(instance), 1);
instance.destroy();
}
}
function addObserver(ctx, tags, cb) {
const destroyMethod = createObserverInstance(() => {
return new SafeObserver(tags, cb);
});
// here need to register destructor, binded to ctx
}
function addEffect(ctx, cb, deps = []) {
const destroyMethod = createObserverInstance(() => {
return new SafeObserver(() => deps, cb);
});
// here need to register destructor, binded to ctx
}
function addAutotrackingEffect(ctx, cb, name) {
const destroyMethod = createObserverInstance(() => {
return new BasicObserver(() => [], cb, name);
});
// here need to register destructor, binded to ctx
}
function watchTag(context, key) {
return function () {
context[key];
}
}
function loop(owner, tickTime = 1000) {
let state = {
isRunning: false,
tickTime,
tick: 0
}
let timer = () => new Promise((resolve) => setTimeout(resolve, state.tickTime));
let afterRender = () => new Promise((resolve) => scheduleOnce('afterRender', resolve));
const renderer = owner.lookup('renderer:-dom');
const revDescriptor = Object.getOwnPropertyDescriptor(renderer, '_lastRevision');
renderer.__lastRevision = revDescriptor.value;
let tickPromise = null;
let tickResolve = () => { };
function newTick() {
tickPromise = new Promise((resolve) => {
tickResolve = resolve;
});
}
function waitForTick() {
return tickPromise;
}
function resolveTick() {
tickResolve();
}
const newDesc = {};
newDesc.get = function () {
return this.__lastRevision;
}
newDesc.set = function (value) {
resolveTick();
newTick();
this.__lastRevision = value;
}
Object.defineProperty(renderer, '_lastRevision', newDesc);
async function run() {
if (state.isRunning) {
return;
}
state.isRunning = true;
await timer();
while (state.isRunning) {
await afterRender();
state.tick++;
for (const observer of Observers) {
try {
observer.recompute();
} catch (e) {
console.error(e, observer.name);
// EOL
}
}
await waitForTick();
//await timer();
}
}
timer().then(run);
return {
async start() {
await this.stop();
run();
},
get isRunning() {
return state.isRunning;
},
get tickTime() {
return state.tickTime;
},
set tickTime(value) {
state.tickTime = value;
},
get tick() {
return state.tick;
},
async stop() {
state.isRunning = false;
await timer();
}
}
}
function effect(klass, property, desc) {
const init = desc.initializer;
desc.initializer = function () {
const result = init.call(this);
let debugName = property;
if ('_debugContainerKey' in this) {
debugName = `${this._debugContainerKey}.${debugName}`;
}
addAutotrackingEffect(this, result, debugName);
return function () {
throw new Error(`Effect on property "${property}" should not be called manually`);
}
}
return desc;
}
export default class ApplicationController extends Controller {
@tracked time = Date.now();
@tracked msg = '';
get appName() {
return new Date(this.time).toLocaleTimeString();
}
@effect onTimeChange = () => {
console.info('on this.time change', this.time);
}
@effect onMsgChange = () => {
console.info('on this.msg change', this.msg);
}
@effect withCustomDeps = () => {
console.info('Im running if time or msg changed');
return [watchTag(this, 'time'), watchTag(this, 'msg')];
}
@effect withCustomDeps2 = () => {
console.info('Im running if time or msg changed (v2)');
Math.random() > 0.5 ? document.body.style.backgroundColor = this.randomColor() : null;
Math.random() > 0.5 ? document.body.style.color = this.randomColor() : null;
Math.random() > 0.5 ? document.body.style.fontSize = (Math.random() * 30 + 30) + 'px' : null;
return () => [this.time, this.msg];
}
randomColor() {
return '#' + Math.random().toString(16).slice(2, 8);
}
constructor() {
super(...arguments);
setInterval(() => {
this.time = Date.now();
}, 1000);
let observerCall = 0;
loop(getOwner(this), 10);
addObserver(this, () => [watchTag(this, 'time')], () => {
observerCall++
this.msg = `Observer called ${observerCall} times`;
});
addEffect(this, () => {
console.log('im side-effect, depend only on tags array');
}, [watchTag(this, 'time')]);
addEffect(this, () => {
console.log(`I'm executing only once, because this logic is not autotracked, value: ${this.time}`);
});
addAutotrackingEffect(this, () => {
console.log(`I'm executing on every time change because automatically consume this.time value: ${this.time}`);
});
}
}
<h1>Welcome to {{this.appName}}</h1>
<br>
<br>
{{outlet}}
<br>
<br>
{{this.msg}}
{
"version": "0.17.1",
"EmberENV": {
"FEATURES": {},
"_TEMPLATE_ONLY_GLIMMER_COMPONENTS": false,
"_APPLICATION_TEMPLATE_WRAPPER": true,
"_JQUERY_INTEGRATION": true
},
"options": {
"use_pods": false,
"enable-testing": false
},
"dependencies": {
"jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.js",
"ember": "3.18.1",
"ember-template-compiler": "3.18.1",
"ember-testing": "3.18.1"
},
"addons": {
"@glimmer/component": "1.0.0",
"tracked-toolbox": "1.2.3"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment