Introducing Spackle
If you’ve shipped a JavaScript library that needs to be the same across every copy of itself in a realm, you’ve probably hit the wall this pattern is built for. Two instances of your module load. Neither knows about the other. Each maintains its own state. The application ends up with two libraries that should have been one, and the bugs that follow are the kind you spend a weekend tracking down.
We have a name for the way out now. We are calling it spackle.
A spackle module races to install a behavior on a shared intrinsic at a registered symbol, and exports an ergonomic function that calls through to whatever got installed. The first instance to load wins the race. Later instances find the property already defined and adopt it instead of installing their own. The pattern is a polyfill and a ponyfill (definition, distinction) and a race discipline rolled into one, and it is what @endo/harden has been quietly doing for a while.
Let’s look at the pattern, walk through why it exists, and see what it looks like in practice.
The Eval Twins Problem
Modules get loaded more than once. Not because anyone wants them to, but because module graphs are messy and bundlers and compartments and version skew all conspire to put two copies of the same code into the same realm. When that happens, you’ve eval twins: two instantiations of the same source running side by side, not recognizing each other.
Most of the things you would use to coordinate across that boundary do not survive it. Unique symbols are not equal across eval twins. Private class fields do not recognize each other. instanceof checks fail. The promise ecosystem ran into this years ago, which is why every Promise implementation eventually converged on duck-typing then methods. It was the only thing that worked when your promise and my promise came from different copies of the same library.
Registered symbols are the exception. Symbol.for('harden') is the same symbol everywhere in the realm, no matter how many times the module that uses it gets loaded. That makes registered symbols a natural rendezvous point. If your eval twins all agree to look at the same well-known location for shared state, they can find each other.
Spackle generalizes this. When a library needs realm-wide identity for something it puts on a shared intrinsic, it uses a registered symbol as the install site and ergonomic exports as the calling convention. First writer wins. Everyone else calls through.
Shim, polyfill, ponyfill, spackle
The shim and polyfill conventions are older than the problem we are solving here, but it is worth setting them next to spackle to see what is new.
A shim or polyfill modifies the global environment so it more closely resembles a later standard. Remy Sharp coined “polyfill” in 2010, borrowing the British brand name Polyfilla, which is spackling paste. The classic guarded install looks like this:
if (!Array.prototype.map) {
Array.prototype.map = function () {
// ...
};
}
It works, mostly. Unconditional overwrite breaks composition with other polyfills. Conditional install risks behavior drift when the native implementation shows up and turns out to differ from the shim. Either way, the install site is a single global slot and the first library to grab it sets the rules.
Sindre Sorhus coined “ponyfill” in 2014 to name the alternative. A ponyfill is a function you import. It checks for native behavior and falls back to a local implementation when the native one is missing. Nothing on the global object gets touched. No race, no composition hazard. The tradeoff is that there is no realm-wide consensus either. My import and your import each make their own decision and live in their own worlds.
Spackle is both. The behavior is installed on a shared intrinsic at a registered symbol, so every copy of the module in the realm sees the same function. An ergonomic callable is also exported, so application code does not have to spell out the symbol every time. The install site is a rendezvous: first instance wins, later instances find the property already there and call through to it.
Because the install lives on Object, which is a shared intrinsic, the behavior is carried into child compartments alongside the intrinsic itself. The pattern survives compartment boundaries.
What @endo/harden does
@endo/harden is the canonical instance. It is also the example that makes the pattern make sense.
harden needs to be realm-wide for two reasons. The first is performance. Each instance of harden maintains a WeakSet of objects it has already frozen, and duplicating that set across eval twins is straightforwardly wasteful. The second is composition with HardenedJS. Applications should be able to use hardened modules whether or not they have called lockdown. If harden behaves differently depending on which copy you got, that promise breaks.
The install lives at Object[Symbol.for('harden')]. The package exports a callable so you do not have to type that out:
import { harden } from '@endo/harden';
harden(object);
Direct use of the installed property is equivalent:
Object[Symbol.for('harden')](object);
Two coordinated behaviors share the install site. If lockdown runs first, it installs a volumetric harden that walks the prototype chain. @endo/harden then defers to whatever lockdown put there. If @endo/harden runs first, it installs a surface harden that freezes the object and its own properties without touching the prototype chain. Surface hardening is what lets hardened modules be useful outside of HardenedJS, because walking the prototype chain on shared intrinsics would have effects similar to lockdown without lockdown’s tenancy-safety repairs.
The install is non-configurable. A later lockdown call detects the corrupted environment and throws. The diagnostic stack points to the module that initialized too early, so the fix is to move that import after lockdown. This is intentional. The pattern is loud when used wrong.
Coming up: @endo/eventual-send
Eventual send is the next member of the family. It needs realm-wide identity, not just realm-wide performance.
Eventual send recognizes and forwards messages through native promises that have been marked at the rendezvous symbol. In the future it will mark non-native promises and presences too. Eval twins of eventual send cannot disagree about which promise or presence is which without losing the ability to deliver messages across the boundary. A spackle install gives eventual send a single source of truth for marked promises, presences, and the operations defined over them.
The same shape is going to land for assert, the causal console, and the rest of the small set of primitives that hardened modules rely on. The point of the pattern is that you can publish a module that uses these primitives without forcing your users to arrange shims. The module works with or without lockdown, inside or outside compartments. That is what hardened modules are.
Leaving room for the language
One last thing about the choice of a registered symbol. It is deliberate.
A future Object.harden could replace Object[Symbol.for('harden')] directly. Alternatively, the committee could introduce a well-known Symbol.harden, distinct from Symbol.for('harden'), so that code running ahead of the specification can tell the registered-symbol install from the well-known one and adapt. Either path stays open. The registered symbol gives implementers a recognizable name without claiming a slot that the language itself might want later.
This matters because the pattern is a transition technology. We are using spackle now because the language does not yet have first-class support for the primitives hardened modules need. When that support arrives, spackle should get out of the way gracefully. Picking a registered symbol over a well-known one is the small piece of forethought that makes the graceful exit possible.
What this enables
This new invention enables us to make hardened modules, and to use them in applications, without arranging shims. Hardened modules are modules that work with or without HardenedJS lockdown, within or without compartments. We are using the spackle pattern to make modules built around harden, eventual-send, assert, errors, and the causal console easier to adopt and use.
If you maintain a library that ships realm-wide behavior on a shared intrinsic, or if you’ve been working around the eval-twins problem with conventions that do not quite hold, this is the shape worth looking at. The full write-up lives in the Endo docs, and @endo/harden is there if you want to read the source of the canonical instance.
Spackle! You heard it here first. The pattern was already there. It just needed a name.
Useful Links
Endo Github Repositories
Spackle.md