Last active
December 9, 2023 15:57
-
-
Save thisredone/4a826f8e5a834f83d5efe7f54a7fcc14 to your computer and use it in GitHub Desktop.
swappable js classess
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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