EMBER.JS: A HISTORICAL ROAST
April 30, 2011. Barack Obama just released his birth certificate. Game of Thrones premiered its first episode. And somewhere, Yehuda Katz looked at SproutCore and said:
"You know what JavaScript needs? More abstraction."
23,057 commits later. Fourteen years. FOURTEEN YEARS.
This framework has outlived:
- Four JavaScript build tools
- Three React rewrites
- The entire lifespan of AngularJS
- Two JavaScript runtimes (Node alternatives)
- My will to live reading this codebase
Let's see what we're working with here.
First line of business - the commit history:
$ git log --oneline --all | tail -1
24f801c64e first commit
$ git log --oneline --all | head -5
f6ad843b26 Merge pull request #21014
That's right. Commit 24f801c64e to commit f6ad843b26. Twenty-three THOUSAND commits of accumulated wisdom.
Or as I like to call it: geological strata of technical debt.
BIT #1: ABANDON ALL HOPE
December 29, 2011. The early days. Ember was still young. Yehuda Katz himself commits:
$ git show a170afafff --stat
commit a170afafff
Author: Yehuda Katz <tomhuda@Yehudas-iMac.local>
Date: Thu Dec 29 17:16:35 2011 -0800
Add note warning future travelers to abandon all hope, ye who enter here
packages/ember-handlebars/lib/helpers/binding.js | 1 +
1 file changed, 1 insertion(+)
Read that commit message again.
"Add note warning future travelers to abandon all hope, ye who enter here."
This isn't a joke. This isn't irony. This is THE CO-CREATOR OF EMBER adding a Dante's Inferno reference to the BINDING HELPERS.
The file's been deleted since. Probably for good reason. But the commit message remains. A warning from 2011. A prophecy, really.
They knew. From the very beginning, THEY KNEW.
And 23,000 commits later, here we are. Still traveling. Still hoping. Still reading code that warns us not to.
BIT #2: THE V8 WHISPERER
packages/@ember/-internals/utils/lib/dictionary.ts:
// the delete is meant to hint at runtimes that this object should remain in
// dictionary mode. This is clearly a runtime specific hack, but currently it
// appears worthwhile in some usecases.
export default function makeDictionary<T>(parent) {
let dict = Object.create(parent);
dict['_dict'] = null;
delete dict['_dict'];
return dict;
}
Let me walk you through this code:
- Create an object
- Add a property called '_dict'
- Set it to null
- IMMEDIATELY DELETE IT
- Return the object
Why? To "hint at runtimes" about dictionary mode.
They're not writing JavaScript. They're WHISPERING to V8. They're sending SUBLIMINAL MESSAGES to the garbage collector. They're doing PERFORMANCE SEANCES.
git blame reveals:
7e77fe3691a (Stefan Penner 2014-07-18)
Stefan Penner. July 2014. ELEVEN YEARS AGO.
Eleven years of developers staring at this code going: "Why does it create and immediately delete a property?"
And the comment says "this is clearly a runtime specific hack."
CLEARLY. They say CLEARLY.
Nothing about this is clear, Stefan. Nothing.
BIT #3: RANDOM QUEUE NAMES
packages/@ember/runloop/index.ts:59:
export const _rsvpErrorQueue = `${Math.random()}${Date.now()}`.replace('.', '');
Let me read that again.
`${Math.random()}${Date.now()}`.replace('.', '')
They're generating a QUEUE NAME using MATH.RANDOM.
Not a UUID library. Not crypto.randomUUID(). Not even a counter.
Math.random() concatenated with Date.now().
And they .replace('.', '') to remove the decimal point. Because apparently "0.7342857342857343" isn't a valid queue name but "073428573428573431702234567890" is TOTALLY FINE.
This is the RSVP ERROR QUEUE. The thing that handles promise rejections. The critical error handling infrastructure.
Named after a random number.
But wait, there's MORE!
packages/@ember/-internals/container/lib/registry.ts:545:
const privateSuffix = `${Math.random()}${Date.now()}`.replace('.', '');
THEY DO IT AGAIN. Different file. Same pattern.
packages/@ember/-internals/utils/lib/symbol.ts:18:
let id = GUID_KEY + Math.floor(Math.random() * Date.now()).toString();
THREE TIMES. Three different places. All generating "unique" identifiers by smashing random numbers together like a drunk person at a keyboard.
"But Math.random() is predictable!" - Some security expert, crying
"But Date.now() could collide!" - Some other engineer, also crying
"Works on my machine." - Ember team, apparently
BIT #4: CLEARLY BROKEN AND WEIRD
packages/@ember/object/mixin.ts:104-110:
// If the super property has a setter, we default to using it no matter what.
// This is clearly very broken and weird, but it's what was here so we have
// to keep it until the next major at least.
//
// TODO: Add a deprecation here.
set = superSetter;
Read that comment again. Slowly.
"This is clearly very broken and weird"
The word "clearly" appears. They KNOW it's broken.
"but it's what was here"
The ancient code demands it.
"so we have to keep it until the next major at least"
At LEAST. They're not even sure.
"TODO: Add a deprecation here."
git blame:
1c1786d2683 (Peter Wagenet 2022-05-17)
That's Peter Wagenet, May 2022. THREE AND A HALF YEARS AGO.
The TODO is still there. The deprecation was never added.
The broken and weird code continues to run in production.
This is the MIXIN SYSTEM. The thing that lets you compose objects.
A FOUNDATIONAL FEATURE of Ember's object model.
And someone wrote, in plain English:
"This is clearly very broken and weird."
And committed it. And it shipped. And it's still there.
Because backwards compatibility is more important than sanity.
BIT #5: JAVASCRIPT SEMANTICS ABUSE
packages/@ember/-internals/glimmer/lib/helpers/unique-id.ts:49-55:
// @ts-expect-error this one-liner abuses weird JavaScript semantics that
// TypeScript (legitimately) doesn't like, but they're nonetheless valid and
// specced.
return ([3e7] + -1e3 + -4e3 + -2e3 + -1e11).replace(/[0-3]/g, (a) =>
((a * 4) ^ ((Math.random() * 16) >> (a & 2))).toString(16)
);
This is how Ember generates unique IDs.
Let me break this down:
[3e7] + -1e3 + -4e3 + -2e3 + -1e11
That's an array containing 30000000, concatenated with negative numbers. JavaScript coerces this to a string: "30000000-1000-4000-2000-100000000000"
Then they REGEX REPLACE the digits 0-3 with random hex.
The comment says it "abuses weird JavaScript semantics." TypeScript "legitimately doesn't like" it. But it's "specced" so it's fine!
This is the {{unique-id}} helper. A PUBLIC API. Documented. Supported. Generating UUIDs through type coercion chaos.
And the source? A gist. From the internet.
// From https://gist.github.com/selfish/fef2c0ba6cdfe07af76e64cecd74888b
They copied UUID generation from a GITHUB GIST. Into a framework used by thousands of production applications.
Someone named "selfish" wrote a one-liner. And now it's Ember's official unique ID strategy.
The gist author's username is literally "selfish." I can't make this up.
BIT #6: PROTOTYPE 1.6 COMPATIBILITY
packages/@ember/array/index.ts:1036-1038:
/**
Invokes the named method on every object in the receiver that
implements it. This method corresponds to the implementation in
Prototype 1.6.
Prototype 1.6.
Let me check the release date of Prototype 1.6.
*checks*
January 2008.
This is 2025 documentation referencing a JavaScript library from ALMOST EIGHTEEN YEARS AGO.
Prototype.js. The framework that peaked when we were still using Internet Explorer 7. When jQuery was the hot new thing. When "AJAX" was a buzzword people used in job interviews.
And Ember's array implementation still says "corresponds to the implementation in Prototype 1.6."
Not "inspired by." Not "similar to." "CORRESPONDS TO."
Present tense. Like Prototype 1.6 is still relevant. Like someone might open the Prototype docs as a reference.
News flash: The last Prototype release was 2015. The project has been dead for A FULL DECADE.
But Ember keeps the flame alive. In their array documentation. For methods that "correspond to" the implementation.
Legacy code has layers. This one has GEOLOGICAL EPOCHS.
BIT #7: TYPESCRIPT VS EMBER
$ grep -rn "ts-expect-error\|@ts-ignore" --include="*.ts" . | wc -l
556
Five hundred and fifty-six TypeScript ignores.
$ grep -rn "as any\|as unknown" --include="*.ts" . | wc -l
125
One hundred and twenty-five type assertions.
Combined: 681 places where TypeScript said "no" and Ember said "yes."
Let's look at some highlights:
packages/@ember/debug/index.ts:69:
// SAFETY: these casts are just straight-up lies
"SAFETY" comment. Followed by "straight-up lies."
packages/@ember/array/index.ts:1403:
// SAFETY: This is not entirely safe and the code will not work with Ember proxies
"SAFETY" comment. Code is "not entirely safe."
packages/@ember/-internals/glimmer/lib/helpers/unique-id.ts:50:
// @ts-expect-error this one-liner abuses weird JavaScript semantics
We covered this one. TypeScript "legitimately doesn't like" it.
But my favorite:
packages/@ember/engine/instance.ts:52:
// type checking, we have broken part of our public API contract.
They KNOW the types break the API contract.
They documented it. In a comment.
And shipped it anyway.
TypeScript exists to catch bugs.
Ember treats it like a nagging spouse to be ignored.
BIT #8: ALLOW_CYCLES
import {
ALLOW_CYCLES,
...
} from '@glimmer/validator';
And later, at line packages/@ember/-internals/metal/lib/computed.ts:424 and packages/@ember/-internals/metal/lib/computed.ts:502:
if (DEBUG) {
ALLOW_CYCLES!.set(propertyTag, true);
}
Let me explain what this is.
Ember's computed property system is supposed to track dependencies. When A depends on B, and B depends on C, everything should work.
But what happens when A depends on B, and B depends on A?
In a sane system: Error. Circular dependency detected. In Ember: ALLOW_CYCLES.
They built a flag. An escape hatch. A "we know this is wrong but let's do it anyway" switch.
And it's only enabled in DEBUG mode! Because in production, cycles should just... work? Silently? Infinitely?
The computed property system was so complex, so deeply nested, that they had to add an official way to say: "Yes, I know this is a cycle. I don't care. Let it burn."
This is reactive programming designed by committee. Over fourteen years. With escape hatches for the laws of causality itself.
BIT #9: ES6TODO - THE ETERNAL WAIT
packages/@ember/-internals/runtime/tests/array/forEach-test.js:42:
// ES6TODO: When transpiled we will end up with "use strict" which
// disables automatically binding to the global context.
ES6TODO.
They made up a special TODO format for ES6.
When did ES6 come out? June 2015. OVER TEN YEARS AGO.
These comments are STILL IN THE CODEBASE.
Still waiting. Still hopeful. Still ES6TODO.
packages/@ember/-internals/runtime/tests/array/map-test.js:42:
// ES6TODO: When transpiled we will end up with "use strict"...
Same comment. Same hope. Same TODO that will never be addressed.
And while we're at "invented TODO formats":
packages/@ember/-internals/metal/lib/computed.ts:604:
// TODO: This class can be svelted once `meta` has been deprecated
git blame:
1c72067ecce (NullVoxPopuli 2019-01-15)
January 2019. ALMOST SEVEN YEARS AGO.
They made up the verb "svelted."
As in "to svelte" - to make smaller.
Named after a different framework entirely.
Almost seven years later, the class has not been svelted.
The meta has not been deprecated.
The TODO remains.
In Ember, TODOs aren't tasks.
They're MONUMENTS.
Tributes to features that will never be implemented.
BIT #10: SORRY NOT SORRY
packages/@ember/array/index.ts:1000-1002:
Note that unlike the other methods, this method does not allow you to
pass a target object to set as this for the callback. It's part of the
spec. Sorry.
This is OFFICIAL DOCUMENTATION.
The word "Sorry" appears in the API docs.
Not "Unfortunately." Not "Due to specification constraints." Just: "Sorry."
Like your friend apologizing for not being able to make your party. Like your coworker saying "sorry" when they accidentally delete your code.
"We couldn't give you a consistent API. Sorry."
This is the REDUCE method. One of the most fundamental array operations. Every other Ember array method lets you pass a target. But reduce? Nope. Spec says no.
Sorry.
And the FIXME just above it:
packages/@ember/array/index.ts:1383:
// FIXME: When called without initialValue, behavior does not match native behavior
git blame:
133b0c15baa (slijack 2022-09-19)
The fix has been needed since September 2022. Over THREE YEARS. The FIXME is still there. The behavior still doesn't match native.
But at least they said sorry.
BIT #11: BUG OR FEATURE?
packages/@ember/-internals/glimmer/tests/integration/components/curly-components-test.js:2603-2625:
// TODO: ember-view is no longer viewable in the classNames array. Bug or
// feature?
let expectedClassNames = ['ember-view', 'foo', 'bar', 'baz'];
assert.ok(
this.$('button').is('.foo.bar.baz.ember-view'),
`the element has the correct classes`
);
// `ember-view` is no longer in classNames.
// assert.deepEqual(clickyThing.get('classNames'), expectedClassNames, ...);
Let me translate this:
- Something changed
- ember-view is no longer in the classNames array
- They don't know if this is intentional
- They commented out the assertion that would fail
- They added "Bug or feature?" as a TODO
- They shipped it
This is the curly-components-test.js file. 3,612 lines of tests. The LARGEST test file in the entire framework.
And buried in it is an existential question: "Bug or feature?"
The assertion is COMMENTED OUT. The code EXPECTS certain behavior. But they're not TESTING for it. Because they don't know if it's RIGHT.
Schrödinger's Test: The assertion is both failing and passing until someone actually runs it and collapses the waveform.
But nobody will run it. Because it's commented out. Forever.
BIT #12: MUTATING ARRAY.PROTOTYPE BY DEFAULT
packages/@ember/object/mixin.ts:644-648:
// Ember.NativeArray is a normal Ember.Mixin that we mix into
// `Array.prototype` when prototype extensions are enabled
// mutating a native object prototype like this should _not_ result
// in enumerable properties being added
//
// _hideKeys disables enumerablity when applying the mixin.
// This is a hack, and we should stop mutating the array prototype
// by default
Let me make sure you understand this.
BY DEFAULT, Ember MUTATES Array.prototype.
As in: the native JavaScript Array.
The one every other library uses.
The one that's supposed to be immutable.
They MIX IN their own methods.
DIRECTLY into Array.prototype.
BY DEFAULT.
And then they have a HACK called _hideKeys to make sure these new methods don't show up when you enumerate.
Because other code (like test frameworks) might do:
for (let key in [])
And suddenly find Ember methods they didn't expect.
The comment even says: "we should stop mutating the array prototype"
git blame shows this comment has been there for years.
They KNOW they should stop.
They haven't stopped.
Every Ember app potentially pollutes the global Array.
With hidden keys.
That are "not enumerable" thanks to a HACK.
This is why we can't have nice things.
BIT #13: THE ROBERT JACKSON SHOW
$ git shortlog -sn --all | head -5
8381 Robert Jackson
2095 Stefan Penner
1622 Peter Wagenet
1446 Katie Gengler
1340 Godfrey Chan
Eight thousand three hundred and eighty-one commits.
The framework started in April 2011. That's roughly 14.5 years. That's 578 commits per year. That's 48 commits per month. That's about 1.6 commits PER DAY.
For FOURTEEN AND A HALF YEARS.
Including weekends. Including holidays. Including the days when his coffee machine broke.
Robert Jackson has committed more code to Ember than some people commit to their marriages.
The next contributor has 2,095 commits. Robert has FOUR TIMES that.
When you git blame a file in Ember, there's a 36% chance Robert Jackson touched it.
(I made up that statistic but it FEELS accurate.)
He's not maintaining Ember. He IS Ember.
If Robert Jackson gets hit by a bus, the framework doesn't get a new maintainer. It gets a FUNERAL.
BIT #14: SHOULD CONTINUE TO FIRE INDEFINITELY
for (let i = 0; i < 10; i++) {
notifyPropertyChange(obj, 'prop');
await runLoopSettled();
}
assert.equal(observerCount, 10, 'should continue to fire indefinitely');
Read that assertion message.
"should continue to fire indefinitely"
This is a TEST. A test that ASSERTS that observers will fire INDEFINITELY.
Not "should fire 10 times." Not "should fire when property changes."
INDEFINITELY.
And the test body? They loop 10 times and call runLoopSettled() 48 different times across this file.
That's right. To test observers, you have to:
- Change a property
- Wait for the run loop to settle
- Check if observers fired
- Repeat
FORTY-EIGHT TIMES in one test file.
The observer system is so asynchronous, so tangled in run loops, that every single test needs to await the universe settling down.
"Indefinitely" isn't a feature. It's a WARNING.
THE FINAL WORD
165,027 lines of JavaScript and TypeScript.
23,057 commits.
556 TypeScript ignores.
Fourteen years.
One codebase.
A codebase where:
- The co-creator committed "abandon all hope" warnings in 2011
- They whisper to V8 by creating and deleting properties
- Queue names are generated with Math.random()
- "Clearly broken and weird" code ships because backwards compatibility
- UUIDs come from a gist by someone named "selfish"
- Documentation references Prototype 1.6 from 2008
- TypeScript ignores are SAFETY comments
- Circular dependencies are allowed via escape hatch
- ES6TODOs wait for a future that arrived 10 years ago
- Official docs say "Sorry."
- Assertions are commented out with "Bug or feature?"
- Array.prototype gets mutated by default
- One person has 36% of all commits
This is Ember.js.
Convention over configuration, they said.
Stability without stagnation, they said.
And somewhere, deep in the codebase, Yehuda Katz's 2011 commit
message echoes through the ages:
"Abandon all hope, ye who enter here."
We entered anyway.
And we're never getting out.