Last active
November 27, 2021 10:58
-
-
Save lifeart/9c81213d298d06a4514a23dbdcb8813a to your computer and use it in GitHub Desktop.
Tracked Observers
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`); | |
}); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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