The code example below shows how to construct a Proxy hierarchy that mimics class and subclass. It accomplishes this by wrapping a standard object with multiple Proxy objects and judicious use of the handler get
option.
Proxy's handler
is a form of the Mix-in Pattern. We can simulate subclassing using the mix-in pattern.
The code contains two types of Proxy "classes": EmitterBase
and EmitterNet
. It uses those to collect statistics specific to a vanilla EventEmitter
or to one of its subclasses Net
. EmitterNet
does not duplicate features of EmitterBase
, but instead reuses Base by wrapping around it. Note, in the example, we wrap http.Server
: a subclass Net and a subsubclass of EventEmitter.
The handler passed to Proxy implements all of the behavior for a subclassed Proxy. Multiple handler versions implement subclasses. For example, in EmitterBase
we collect statistics on calls to on
and emit
(count the number of calls). EmitterBase
also implements phantom members and methods to track and report on those counts. This is the equivalent of a base class with those methods and members. See the handler
in wrapEmitterBase
.
Next, we create a Proxy "subclass" using another handler (see wrapEmitterNet
) which implements two new phantom members for counting Net specific calls (listen
and close
). It also implements a method stats()
that overrides a method from the base class as well as calling the overridden method.
The Proxy standard gives us enough features to implement Proxy subclassing without resorting to class wrappers and messing with this
.
import * as util from 'node:util';
import http from 'node:http';
import { EventEmitter } from 'node:events';
async function DemoProxyHierarchy()
{
const greeter = wrapEmitterBase(new EventEmitter());
greeter.on("hello", (person) => console.log((`Hello, ${person}!`)));
greeter.emit("hello", "World");
greeter.emit("hello", "Benjamin");
console.log(`on calls: ${greeter.countOn}`);
console.log(`emit calls: ${greeter.countEmit}`);
console.log(`statistics: ${JSON.stringify(greeter.stats())}`);
const stats = new Promise((Resolve, reject) => {
let steps = 0;
const server = http.createServer((req, res) => { res.end() });
const netWrapper = wrapEmitterNet(server) as any;
const done = () => {
if (++steps > 2) {
console.log(`\non calls: ${netWrapper.countOn}`);
console.log(`emit calls: ${netWrapper.countEmit}`);
netWrapper.close(() => Resolve(netWrapper.stats()));
}
};
netWrapper.listen(8080, done);
http.get('http://localhost:8080', done);
http.get('http://localhost:8080', done);
});
return stats.then(s => console.log(`net stats: ${JSON.stringify(s)}`));
}
function wrapEmitterBase(ee: EventEmitter)
{
const stats = { on: 0, emit: 0 };
const handler = {
get: (target, key) => {
switch (key) {
case "countOn": return stats.on;
case "countEmit": return stats.emit;
case "stats": return () => ({ ...stats });
case "on": { stats.on++; break; }
case "emit": { stats.emit++; break; }
}
return target[key];
},
}
return new Proxy(ee, handler);
}
function wrapEmitterNet(ee: EventEmitter)
{
const stats = { listen: 0, close: 0 };
const handler = {
get: (target, key) => {
switch (key) {
case "stats": {
return () => ({ ...target[key](), ...stats });
}
case "listen": { stats.listen++; break; }
case "close": { stats.close++; break; }
}
return target[key];
},
};
return new Proxy(wrapEmitterBase(ee), handler);
}
// IIFE
(()=> { await DemoProxyHierarchy() })();
/* Output:
Hello, World!
Hello, Benjamin!
on calls: 1
emit calls: 2
statistics: {"on":1,"emit":2}
on calls: 1
emit calls: 5
net stats: {"on":2,"emit":6,"listen":1,"close":1}
*/