Skip to content

Instantly share code, notes, and snippets.

@thisredone
Last active December 9, 2023 15:57
Show Gist options
  • Save thisredone/4a826f8e5a834f83d5efe7f54a7fcc14 to your computer and use it in GitHub Desktop.
Save thisredone/4a826f8e5a834f83d5efe7f54a7fcc14 to your computer and use it in GitHub Desktop.
swappable js classess
self._swappableClasses ??= {
existing: {},
instances: {},
bodyStrings: {},
ancestralHooks: {},
};
const isPromise = (v) =>
v != null && (v instanceof Promise || (typeof v === 'object' && typeof v.then === 'function'))
const errorOnce = function (name, fn) {
return function (...args) {
this.__swappableDisabled ??= {};
if (this.__swappableDisabled[name]) return;
let res;
try {
res = fn.call(this, ...args);
if (isPromise(res)) res = res.catch(err => {
this.__swappableDisabled[name] = true;
log('[swappable errorOnce]', err);
});
} catch (err) {
this.__swappableDisabled[name] = true;
log('[swappable errorOnce]', err);
}
return res;
}
}
self.swappable ??= (name, _class, opt) => {
if (opt) [_class, opt] = [opt, _class]; else opt = {};
const existing = _swappableClasses.existing[name] ??= [],
instances = _swappableClasses.instances[name] ??= [],
previousString = _swappableClasses.bodyStrings[name],
currentString = _class.toString(),
canSwap = typeof _class.prototype.swap === 'function' ||
_class.prototype.__swappableErrorOnce ||
currentString.includes('onMany'),
changed = opt.assumeChanged || previousString != null && previousString != currentString,
hooks = _swappableClasses.ancestralHooks[name] ??= [];
let proxied = _class;
_swappableClasses.bodyStrings[name] = currentString;
_class.__swapName = name;
_class.prototype.__swappableErrorOnce?.forEach(fnName =>
_class.prototype[fnName] = errorOnce(fnName, _class.prototype[fnName]));
const swapInstances = instances =>
setTimeout(() => {
for (let i = instances.length - 1; i >= 0; i--) {
const instance = instances[i].deref();
// if (!instance) log('instance was garbage collected', name);
if (!instance) instances.splice(i, 1);
else {
instance.__defaultSwappableSwap?.();
if (instance.swap?.() == '+delete+') instances.splice(i, 1);
}
}
}, 0);
if (canSwap) {
_class.prototype.onMany = function(scope, handlers) {
this._myEventedCallbacks ??= [];
Object.entries(handlers).forEach(([eventName, callback]) => {
scope.on(eventName, callback, this);
this._myEventedCallbacks.push([scope, eventName, callback]);
});
};
_class.prototype.__defaultSwappableSwap = function() {
this.__swappableDisabled = {};
const hasEvents = this._myEventedCallbacks != null;
if (hasEvents) {
for (let [scope, eventName, callback] of this._myEventedCallbacks)
scope.off(eventName, callback);
this._myEventedCallbacks = [];
}
this.handleEvents?.();
};
}
if (canSwap || _class.prototype._constructor)
proxied = new Proxy(_class, { construct(target, args, newTarget) {
const res = Reflect.construct(target, args, newTarget);
res._constructor?.(...args);
instances.push(new WeakRef(res));
return res;
}});
if (existing.length === 0) {
if (currentString.startsWith('class extends')) {
let parent = _class.__proto__;
while (parent && parent.__swapName) {
_swappableClasses.ancestralHooks[parent.__swapName].push(name);
parent = parent.__proto__;
}
}
}
if (changed) {
const proto = _class.prototype,
props = Object.entries(Object.getOwnPropertyDescriptors(proto));
for (let i = existing.length - 1; i >= 0; i--) {
const e = existing[i].deref();
if (e) {
for (let [key, desc] of props) {
if (desc.value) e.prototype[key] = desc.value;
else if (desc.get) e.prototype.__defineGetter__(key, desc.get);
else if (desc.set) e.prototype.__defineSetter__(key, desc.set);
}
for (let key of Object.getOwnPropertyNames(e))
if (!['length', 'prototype', 'name'].includes(key))
if (_class[key] && !e[key] || typeof e[key] === 'function')
e[key] = _class[key];
} else {
existing.splice(i, 1);
// log(`class ${ name }[${ i }] was garbage collected`);
}
}
if (canSwap) swapInstances(instances);
hooks.forEach(childName => swapInstances(_swappableClasses.instances[childName]));
}
existing.push(new WeakRef(_class));
return proxied;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment