Monkey patching in JavaScript
Beyond window
“In computer programming, monkey patching is a technique used to dynamically update the behavior of a piece of code at run-time”
Sometimes, especially in JavaScript, you’ll run into problems that are unsolvable without changing some existing code. And often that code is outside your app — in a library — where your only ways of changing it are:
Send a PR and hope the maintainer merges it
Fork
Forking can be reasonable for smaller libraries especially if you expect to make more changes in the future. I remember forking Turbolinks in the past (after it was deprecated but still worked great for my use case with some changes). Also many genuinely small libraries should never be used at all.
Back to monkey patching. There are cases where forking isn’t feasible — it’s still an actively maintained library and you only want to make small changes, not maintain the whole thing yourself — and for whatever reason your changes can’t be added to the library. Either it’s something the maintainer personally doesn’t want, something that should only be added in a future major version, or things are just taking time.
In those cases, you’re only left with monkey patching. This should be pretty much a last resort since any future update of the library can break your code — every update essentially becomes a major version upgrade — so from the moment you patch something, you need to review every future update to see if everything still works. But as much as You’re Not Meant To Do This, You Can’t Do This, Serious Projects Wouldn’t Use This, it’s pretty much the best you’ve got.
I’m writing this mainly to document some techniques you can use that aren’t all that well known. The key thing we’ll get to is that even if some code/data isn’t accessible by global roots or objects in scope, you may still be able to change it if you know the usage pattern of said code/data. That’s alotta words that won’t make much sense just yet, so let’s get to some examples.
Basic stuff
Before getting to the more evil techniques, let’s start with the obvious stuff: if you can access some code by global roots (i.e. window.whatever objects), or stuff that’s otherwise in scope of code you control, you can just change any objects contained within it that are accessible to you.
For example, in Tenancy v4 we have a feature for tenant-specific broadcasting — each tenant gets a separate channel. Then, to create tenant-aware clones of already registered channels in Laravel Echo, we can do this:
if (tenantChannelPrefix) {
Object.keys(window.Echo.connector.channels).forEach(channel => {
// Don't clone global channels
if (channel.startsWith('global__')) {
return;
}
let tenantChannel = null;
if (channel.startsWith('private-encrypted-')) {
tenantChannel = window.Echo.privateEncrypted(tenantChannelPrefix + channel.split('-')[2]);
} else if (channel.startsWith('private-')) {
tenantChannel = window.Echo.private(tenantChannelPrefix + channel.split('-')[1]);
} else if (channel.startsWith('presence-')) {
tenantChannel = window.Echo.presence(tenantChannelPrefix + channel.split('-')[1]);
} else {
tenantChannel = window.Echo.channel(tenantChannelPrefix + channel);
}
// Give the tenant channel the original channel's callbacks (listen() etc)
tenantChannel.subscription.callbacks._callbacks = window.Echo.connector.channels[channel].subscription.callbacks._callbacks
});
}This isn’t “full” monkey patching in a way, we are just changing data, not code, but I’d still count it as monkey patching. We are essentially changing some library-managed object while bypassing the public API.
Object.prototype
Now we get to the real hack I want to show in this article.
You can access pretty much any object or array if you know how it’s being used.
Here’s what I mean by that:
let myObject = {};
Object.defineProperty(Object.prototype, 'foobar', {
get() {
// We have to use a different property, _foobar,
// since accessing .foobar would just recursively
// call this getter infinitely.
return this._foobar || "NOT FOUND";
},
set(value) {
this._foobar = value.toUpperCase();
}
});
console.log(myObject.foobar); // NOT FOUND
myObject.foobar = 'abc';
console.log(myObject.foobar); // ABC
// we don't have to use myObject, we can use ANY object
console.log(console.foobar); // NOT FOUND
console.foobar = 'def';
console.log(console.foobar); // DEFWe can define a property on Object.prototype which makes the property available on every object. The “value” for the object is something like a proxy — a getter and a setter.
Similarly, for arrays we can use Array.prototype.
Now, the key point is: if we know how a variable is used — if we know its usage pattern from inspecting either its source code, like in this case, or stepping through a debugger — we can intercept it.
This is possible because of JavaScript’s prototype-based OOP model.
Prototypes are essentially global roots that let us change any and every object even if it’s a completely local variable that’d otherwise be inaccessible.
The first example I’ll give of this is updating an entry in a local, generally inaccessible array. It does, however, have a public function for adding entries to it. Just not getting existing ones and updating them, which is what we’re trying to do.
Livewire provides lifecycle hooks in its JS API. If we look at the implementation of the hook() method:
// livewire/js/index.js
import { on as hook, trigger, triggerAsync } from './hooks'
import { directive } from './directives'
import Alpine from 'alpinejs'
let Livewire = {
directive,
dispatchTo,
start,
first,
find,
getByName,
all,
hook, // <----- this
trigger,
triggerAsync,
dispatch,
on,
get navigate() {
return Alpine.navigate
}
}We see it’s coming from livewire/js/hooks.js, specifically the on() function inside that file. Let’s take a look there:
/**
* Our internal event listener bus...
*/
let listeners = []
/**
* Register a callback to run when an event is triggered...
*/
export function on(name, callback) {
if (! listeners[name]) listeners[name] = []
listeners[name].push(callback)
// Return an "off" callback to remove the listener...
return () => {
listeners[name] = listeners[name].filter(i => i !== callback)
}
}Now, I have a use case where I need to modify (not just add to) something in the listeners array. It’s a local variable, completely inaccessible by global roots. Namely, I’ve found that Livewire’s logic for downloading files is implemented as a listener here, and I need to change it in my app.
What I can do is just define a __livewireHookDiscovery property on Array.prototype — meaning all arrays — and then use the public Livewire.hook() method to add a __livewireHookDiscovery key to the listeners array.
Object.defineProperty(Array.prototype, '__livewireHookDiscovery', {
get() { window.__livewireHooks = this; return []; },
set() { window.__livewireHooks = this; }
});
document.addEventListener('livewire:init', () => Livewire.hook('__livewireHookDiscovery'));In the context of the proxy, this is listeners! That’s how we get access to it.
Now I know that after livewire:init, window.__livewireHooks will point to the listeners array from the hooks.js file.
let downloadHook = window.__livewireHooks.commit.map((hook: Function) => hook.toString()).findIndex((hook: string) => {
// In the past we just checked
// hook.includes('createObjectURL(base64toBlob')
// but now there are .min.js builds in prod, so we can only search for keywords that don't get minified
return hook.includes('createObjectURL') &&
hook.includes('.contentType') &&
hook.includes('.revokeObjectURL');
});
if (downloadHook === -1) {
// handle error — this likely means a version update broke my hook
}
window.__livewireHooks.commit[downloadHook] = ({ succeed }) => {
succeed(({ effects }) => {
if (effects.download) {
let file: {name: string, content: string} = effects.download;
// handle my download here
}
});
};Not my proudest code. But also the least evil thing I can do here. It is what it is. That’s kind of the purpose of this article, you know.
Example 2: Object.prototype
This is another example of the approach used above. This time we’re using Object.prototype instead of Array.prototype. Those two are basically the only two prototypes you’d use for these things since most variables you want to hook either are either based on Object.prototype or are arrays.
The goal of the following example: make Livewire’s navigate cache temporary. The current behavior is that when you use wire:navigate.hover, all page prefetches are cached forever, until you hard refresh the page or “leave” the wire:navigate context by clicking a non-wire:navigate link, causing a full page load, clearing page cache stored in some runtime JS objects. All I’m trying to do is this, but since the PR still isn’t merged, we gotta take some initiative here since this is pretty much a critically important feature for me and I specifically upgraded to Livewire 3 to be able to use wire:navigate. I want the on-hover prefetching! It makes my app’s UX so much better.
So, looking at the Livewire source code again, we see:
let prefetches = {}
export function prefetchHtml(destination, callback) {
let uri = getUriStringFromUrlObject(destination)
if (prefetches[uri]) return
prefetches[uri] = { finished: false, html: null, whenFinished: () => {} }
performFetch(uri, (html, routedUri) => {
callback(html, routedUri)
})
}In some cases whenFinished is filled with some internal callbacks but I don’t want to spend too much time explaining the code here. The key thing is that in cases I care about, it’s set to the default empty handler, and it’s the perfect callback for us to add some logic like:
// Prune prefetch after 10 seconds
whenFinished: () => setTimeout(() => delete prefetches[uri], 10 * 1000)Again, none of this code is globally accessible, it’s all local code. And this time there isn’t even any hook API we could use to easily interact with the property in at least some way — the prefetch is added by Livewire internally and we can’t add in any function.
What we can do, however, is add a hook to… every JS object under the sun.
Now, to solve this prefetch situation, we have two options:
Create a hook on
Object.prototype, which will apply to the prefetches variable, to interceptthis(it’s available inget()/set()), store that in some global variable and then at other points in the lifecycle (after each prefetch is created) add thewhenFinishedcallback we want.Same thing but directly add
whenFinishedin the setter.
Option 1 may seem cleaner — we only abuse the prototype to intercept the prefetches object, remove the property definition(s) to avoid polluting our prototypes further, and do the rest in some kind of listener. But ultimately it’s more code and all of the code involved in this dirty trick is ugly as hell, so I’m going with option 2 since it’s just less code.
First, some boilerplate:
let prunePrefetchesAfterTenSeconds = () => {
// all possible keys for `prefetches`: in my case `['/orders', '/bank', '...']` (nav links) and any other links that may be on this particular page
let links = Array.from(document.querySelectorAll('[wire\\:navigate\\.hover]')).map(a => a.href).filter(a => a).map(s => (new URL(s)).pathname);
for (let link of links) {
// continued below
}
};
// Wrapper around what'd normally be DOMContentLoaded, but in the case of wire:navigate, changed to livewire:navigated. Just a lil convenient abstraction
window.domReady = (callback) => document.addEventListener('livewire:navigated', callback);
domReady(() => prunePrefetchesAfterTenSeconds());Then, for each link we define a property on Object.prototype:
// If Object.prototype already has a property with the name of `link`, we don't mess with it.
if (Object.prototype.hasOwnProperty(link)) continue;
Object.defineProperty(Object.prototype, link, {
set(value) {
// this = prefetches
// link = `/orders`
// value = { html: string, finished: bool, whenFinished: function }
// Again, we have to use a separate property for the actual value
this[`_${link}`] = value;
// Condition to avoid messing up other objects that might have the same keys as `prefetches`.
// This just confirms that we are *certainly* (well, almost) working with `prefetches` here
if (value.hasOwnProperty('finished') && value.hasOwnProperty('whenFinished') && value.hasOwnProperty('html')) {
// Our actual code!
setTimeout(() => delete this[`_${link}`], 10*1000);
}
},
get() {
// Same custom property as used in set()
return this[`_${link}`];
}
});
And just like that, any property that’s added to prefetches will automatically get deleted 10 seconds after being added.
No need for other separate listeners, hooks, or anything like that.
We can now easily confirm that hovering on a link will make a request, in the Network tab of Developer Tools. Then for 10 seconds, any hovers on that link will not do anything. And after 10 seconds, the first hover will again make a request. And so on.
So our data never gets stale for more than 10 seconds, providing all the benefits of prefetching on hover — near instant loads upon clicking — without the data ever getting too stale.
