Last active
December 8, 2021 18:42
-
-
Save mhofman/f5ac91ee630c9876fd9bf997f8aa499c to your computer and use it in GitHub Desktop.
Wrap any value into a registered object
This file contains 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
export declare class Wrapper<Kind, Value = any> { | |
private kind: Kind; | |
private value: Value; | |
} | |
export interface WrapperRegistry<Kind> extends Function { | |
constructor: WrapperRegistryConstructor; | |
wrap<T>(value: T): Wrapper<Kind, T>; | |
unwrap<T>(wrapped: Wrapper<Kind, T>): T; | |
} | |
export interface WrapperRegistryConstructor { | |
new <T extends string | number>(description?: T): WrapperRegistry<T>; | |
isWrapper(wrapper: any): wrapper is Wrapper<any>; | |
} | |
export const WrapperRegistry: WrapperRegistryConstructor; |
This file contains 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
const testSet = new WeakSet(); | |
const hasObject = (value) => { | |
const type = typeof value; | |
switch (type) { | |
case "object": | |
if (!value) return false; | |
// fallthrough | |
case "box": | |
case "function": | |
return true; | |
case "boolean": | |
case "number": | |
case "string": | |
case "symbol": | |
case "bigint": | |
case "undefined": | |
return false; | |
case "record": | |
case "tuple": | |
// Use Box.containsBox or other predicate | |
// fallthrough for now | |
default: | |
try { | |
testSet.add(value); | |
testSet.delete(value); | |
return true; | |
} catch (err) {} | |
return false; | |
} | |
}; | |
export const WrapperRegistry = (() => { | |
const wrapperBrand = Symbol("Wrapper"); | |
const WrapperBase = (() => | |
class { | |
#brand = wrapperBrand; | |
static [Symbol.hasInstance](wrapper) { | |
try { | |
return wrapper.#brand === wrapperBrand; | |
} catch (err) { | |
return false; | |
} | |
} | |
})(); | |
delete WrapperBase.prototype.constructor; | |
function WrapperRegistry(description) { | |
if (!new.target) { | |
throw new TypeError(); | |
} | |
const wrapperKind = Symbol(description); | |
const minuszero = Symbol("-0"); | |
let internalNew = false; | |
const primitiveWrappers = new Map(); | |
const objectWrappers = new WeakMap(); | |
const fr = new FinalizationRegistry((held) => { | |
let wrappers; | |
let value; | |
if (held === null || typeof held !== "object") { | |
wrappers = primitiveWrappers; | |
value = held; | |
} else { | |
wrappers = objectWrappers; | |
value = held.deref(); | |
} | |
const wr = wrappers.get(value); | |
if (wr && !wr.deref()) { | |
wrappers.delete(value); | |
} | |
}); | |
class Wrapper extends WrapperBase { | |
#kind = wrapperKind; | |
#value; | |
constructor(value) { | |
super(); | |
if (!internalNew) { | |
throw new TypeError(); | |
} | |
this.#value = value; | |
} | |
static wrap(value) { | |
if (Object.is(-0, value)) value = minuszero; | |
const valueHasObject = hasObject(value); | |
const wrappers = valueHasObject ? objectWrappers : primitiveWrappers; | |
let wr = wrappers.get(value); | |
let wrapper = wr && wr.deref(); | |
if (!wrapper) { | |
if (wr) { | |
fr.unregister(wr); | |
} | |
try { | |
internalNew = true; | |
wrapper = Object.freeze(new Wrapper(value)); | |
} finally { | |
internalNew = false; | |
} | |
wr = new WeakRef(wrapper); | |
const held = valueHasObject ? new WeakRef(value) : value; | |
fr.register(wrapper, held, wr); | |
wrappers.set(value, wr); | |
} | |
return wrapper; | |
} | |
static unwrap(wrapper) { | |
const value = wrapper.#value; | |
return value === minuszero ? -0 : value; | |
} | |
static [Symbol.hasInstance](wrapper) { | |
try { | |
return wrapper.#kind === wrapperKind; | |
} catch { | |
return false; | |
} | |
} | |
} | |
delete Wrapper.prototype.constructor; | |
return Wrapper; | |
} | |
Object.defineProperty(WrapperRegistry, "isWrapper", { | |
value: WrapperBase[Symbol.hasInstance], | |
writable: true, | |
configurable: true, | |
}); | |
Object.setPrototypeOf(WrapperRegistry.prototype, Function.prototype); | |
Object.setPrototypeOf(WrapperBase, WrapperRegistry.prototype); | |
return WrapperRegistry; | |
})(); |
This file contains 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
// @ts-check | |
import assert from "assert"; | |
import { WrapperRegistry } from "./wrapper-registry.js"; | |
const allCollections = []; | |
const fr = new FinalizationRegistry((resolve) => { | |
resolve(); | |
}); | |
const addCollection = (obj) => | |
allCollections.push(new Promise((resolve) => fr.register(obj, resolve))); | |
const wm = new WeakMap(); | |
const FooWrapper = new WrapperRegistry("Foo"); | |
const BarWrapper = new FooWrapper.constructor("Bar"); | |
{ | |
assert(FooWrapper instanceof WrapperRegistry); | |
const fooWrapped = FooWrapper.wrap(42); | |
assert(typeof fooWrapped === "object"); | |
assert(FooWrapper.wrap(42) === fooWrapped); | |
assert(FooWrapper.unwrap(fooWrapped) === 42); | |
assert(fooWrapped instanceof FooWrapper); | |
assert(WrapperRegistry.isWrapper(fooWrapped)); | |
addCollection(fooWrapped); | |
assert(BarWrapper instanceof WrapperRegistry); | |
const barWrapped = BarWrapper.wrap(42); | |
// @ts-expect-error | |
assert(barWrapped !== fooWrapped); | |
assert(barWrapped instanceof BarWrapper); | |
assert(!(fooWrapped instanceof BarWrapper)); | |
addCollection(barWrapped); | |
// @ts-expect-error | |
assert.throws(() => BarWrapper.unwrap(fooWrapped)); | |
// @ts-expect-error | |
assert.throws(() => FooWrapper.unwrap(barWrapped)); | |
const fooBarWrapped = FooWrapper.wrap(barWrapped); | |
wm.set(barWrapped, fooBarWrapped); | |
assert(FooWrapper.wrap(barWrapped) === fooBarWrapped); | |
addCollection(fooBarWrapped); | |
// @ts-expect-error | |
assert.throws(() => BarWrapper.unwrap(fooBarWrapped)); | |
const barFooWrapped = BarWrapper.wrap(fooWrapped); | |
wm.set(fooWrapped, barFooWrapped); | |
assert(BarWrapper.unwrap(barFooWrapped) === fooWrapped); | |
addCollection(barFooWrapped); | |
// @ts-expect-error | |
assert.throws(() => FooWrapper.unwrap(barFooWrapped)); | |
} | |
const queueGCJob = () => new Promise((resolve) => setTimeout(resolve, 0)); | |
const readyGC = () => { | |
new Promise((resolve) => fr.register({}, resolve)).then(() => | |
console.log("sentinel collected") | |
); | |
return queueGCJob(); | |
}; | |
const allCollected = Promise.all(allCollections).then(() => { | |
console.log("all wrappers collected"); | |
return true; | |
}); | |
const cleanup = async (tries = 0) => { | |
const result = await Promise.race([ | |
allCollected, | |
readyGC().then(() => false), | |
]); | |
if (result) { | |
return; | |
} | |
console.log("GC attempt", ++tries); | |
if (typeof gc === "function") { | |
gc(); | |
} else { | |
const arr = Array.from({ length: 2 ** 24 }, () => Math.random()); | |
} | |
if (tries > 10) throw new Error(); | |
return cleanup(tries); | |
}; | |
export const result = cleanup().then(async () => { | |
// Make sure all Wrapper's internal pending finalization callbacks are called | |
await queueGCJob(); | |
console.log("clean"); | |
// Keep registries in scope till the end | |
const fooWrapped = FooWrapper.wrap(42); | |
const barWrapped = BarWrapper.wrap(42); | |
// Without a read from the WeakMap, JSC aggressively optimizes collection | |
const hasAny = wm.has(fooWrapped) || wm.has(barWrapped); | |
const fooBarWrapped = FooWrapper.wrap(barWrapped); | |
const barFooWrapped = BarWrapper.wrap(fooWrapped); | |
console.log("done"); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This would be useful in the context of Tagged Records