5

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 a number.
  • During the first "run", because of the whole proxy thing, res1 will point to a placeholder object: Proxy { _value: undefined }
  • The real lib is loaded, "playback" finishes, ourRealLib.getThat(3) returns 23, so res1 will be Proxy { _value: 23 }
  • Can I do anything so that we can use res1 as a number? Something like:
console.log(res1 * 2); // 46
Tibi Neagu
  • 391
  • 1
  • 10
  • 1
    no. You cannot. Also I doubt you really want that. Your example would work with `res1 = { getValue() { this._value; } };` though. – Jonas Wilms Mar 19 '20 at 18:09
  • 1
    @JonasWilms `getValue` or `valueOf` ? – Tibi Neagu Mar 19 '20 at 18:24
  • 1
    valueOf, it's late ... – Jonas Wilms Mar 19 '20 at 18:24
  • @JonasWilms the `valueOf` advice seems to do the job for me. It even works if `_value` is an actual object, not just primitives. Why do you say you doubt I really want that? Are there any downsides to using it like that? – Tibi Neagu Mar 19 '20 at 18:44
  • yes, I doubt your whole approach. I guess in the end rewriting the whole thing is much less pain – Jonas Wilms Mar 19 '20 at 18:49
  • 1
    The approach of caching calls only works for Google Analytics because they are known not to return anything. – Bergi Mar 19 '20 at 19:51
  • Possible duplicate of [How to proxy JavaScript creation primitive](https://stackoverflow.com/q/60383942/1048572)? – Bergi Mar 19 '20 at 19:52

0 Answers0