Skip to content

Instantly share code, notes, and snippets.

@mhofman
Last active December 8, 2021 18:42
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mhofman/f5ac91ee630c9876fd9bf997f8aa499c to your computer and use it in GitHub Desktop.
Save mhofman/f5ac91ee630c9876fd9bf997f8aa499c to your computer and use it in GitHub Desktop.
Wrap any value into a registered object
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;
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;
})();
// @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");
});
@mhofman
Copy link
Author

mhofman commented Dec 8, 2021

This would be useful in the context of Tagged Records

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment