I'm trying to build a system that "caches" calls to a library before that library has loaded.
This is similar to what the Google Analytics "setup" code is doing with the _gaq
variable - it's initialized as an array that "caches" calls to the real analytics endpoints until the ga.js
library has loaded. When it does, it reads _gaq
and replays the calls.
We decided to do this because our legacy code contains a lot of calls to a particular library which is loaded synchronously in <head>
. This greatly increases the time to first contentful paint, as a lot of JS is evaluated and executed.
However, there are too many places in the code that would need to be changed (wrapped in a 'DOMContentLoaded'
listener), so we decided to try using workaround.
We decided to try using Proxy
to catch calls to our library's methods, and replay them once it's ready:
// Original code:
var ourLib = new OurLib({ ... });
// Throughout the site, calls such as:
var res1 = ourLib.doThis();
var res2 = ourLib.getThat(3);
Here's a sort-of "simplified" version of what our new code is doing:
// New code:
var ourLib = new Proxy({
calls: [],
}, {
get(target, prop) {
if (prop in target) {
return Reflect.get(...arguments);
}
const callref = { prop, args: [], placeholder };
target.calls.push(callref);
return function(...args) {
const placeholder = MakeResultPlaceholder(...);
callref.args = args;
callref.placeholder = placeholder;
return placeholder;
};
},
});
// Throughout the site, calls continue as before, except now they're 'stored' in `calls`
var res1 = ourLib.doThis();
var res2 = ourLib.getThat(3);
// Much later, the original lib is loaded, and
var ourRealLib = new OurLib({ ... });
__playbackCalls(ourLib.calls, ourRealLib);
// Replace the proxy with the real thing
ourLib = ourRealLib;
After the run above, the calls
property will be something like this:
[
{
prop: 'doThis',
args: [],
reference: ResultPlaceholder
},
{
prop: 'getThat',
args: [3],
reference: ResultPlaceholder
}
]
The __playbackCalls
function iterates through the calls
array and apply
each method from ourRealLib
with the args
stored in each object.
calls.forEach(({ prop, args, reference }) => {
reference._value = ourRealLib[prop].apply(ourRealLib, args);
});
The problem comes when the result of the proxy's calls needs to be used. Right now, as you can see, the calls return a placeholder
object (which is in itself another proxy). These placeholders hold a _value
property that becomes populated during "playback".
So here's the question:
- Let's say
ourLib.getThat()
is meant to return anumber
. - During the first "run", because of the whole proxy thing,
res1
will point to aplaceholder
object:Proxy { _value: undefined }
- The real lib is loaded, "playback" finishes,
ourRealLib.getThat(3)
returns23
, sores1
will beProxy { _value: 23 }
- Can I do anything so that we can use
res1
as a number? Something like:
console.log(res1 * 2); // 46