Here's a TupleSet in JS. How can we make it a WeakTupleSet?

41 views Asked by At

Here's a JavaScript TupleSet, i.e. a set that contains unique tuples where order within each tuple does not matter (similar in some ways to MultiKeyMap in Java):

export class TupleSet {
    tuples = new Set<Array<unknown>>()

    add(tuple: Array<unknown>) {
        return this.#findTuple(
            tuple,
            () => false, // found (not added)
            () => {
                this.tuples.add(tuple)
                return true // not found (added)
            },
        )
    }

    delete(tuple: Array<unknown>) {
        return this.#findTuple(
            tuple,
            tuple => {
                this.tuples.delete(tuple)
                return true // found (deleted)
            },
            () => false, // not found (not deleted)
        )
    }

    has(tuple: Array<unknown>) {
        return this.#findTuple(
            tuple,
            () => true,
            () => false,
        )
    }

    #findTuple(tuple: Array<unknown>, onFound: (foundTuple: Array<unknown>) => boolean, onNotFound: () => boolean) {
        outer: for (const testTuple of this.tuples) {
            if (tuple.length !== testTuple.length) continue
            for (const value of tuple) if (!testTuple.includes(value)) continue outer
            return onFound(testTuple)
        }

        return onNotFound()
    }
}

How can we make a weak TupleSet (named WeakTupleSet) such that if at least one item in a tuple is no longer referenced outside of the WeakTupleSet, the tuple is automatically removed from the WeakTupleSet?

(If the user code outside of the set does not have all the original references to the items in a tuple, it is impossible to use that tuple again, hence the tuple should be removed from the set)

Example:

const set = new WeakTupleSet()

globalThis.obj1 = {}
globalThis.obj2 = {}
globalThis.obj3 = {}

const tuple = [globalThis.obj1, globalThis.obj2, globalThis.obj3]

set.add(tuple)

set.has(tuple) // true

// later, unreference obj2
tuple[1] = undefined
delete globalThis.obj2

// even later, the set should no longer contain the tuple, and obj2 is fully garbage collected

I have a feeling a WeakRef dance may be possible.

1

There are 1 answers

0
Bergi On

I don't think you'll need WeakRefs, but a WeakSet would come in handy and with a known size it could be used for comparisons. Then use a FinalizationRegistry as you planned to remove incomplete entries from your set.

class WeakEntry<T> extends WeakSet<T> {
    constructor(elements) {
        super(elements);
        this.size = (elements instanceof Set ? elements : new Set(elements)).size;
    }
    equals(other: Set<T>): boolean {
        if (this.size != other.size) return false;
        for (const el of other) if (!this.has(el)) return false;
        return true;
    }
}
export class WeakSetSet<T extends object> {
    #entries = new Set<WeakEntry<T>>();
    #cleanup = new FinalizationRegistry(entry => {
        this.#cleanup.unregister(entry);
        this.#entries.delete(entry);
    });

    add(values: Set<T>): boolean {
        for (const entry of this.#entries) if (entry.equals(values)) {
            return false; // found (not added)
        }
        const newEntry = new WeakEntry(values);
        for (const value of values) {
            this.#cleanup.register(value, newEntry, newEntry);
        }
        this.#entries.add(newEntry)
        return true; // not found (added)
    }
    delete(values: Set<T>): boolean {
        for (const entry of this.#entries) if (entry.equals(values)) {
            this.#entries.delete(entry);
            return true; // found (deleted)
        }
        return false; // not found (not deleted)
    }
    add(values: Set<T>): boolean {
        for (const entry of this.#entries) if (entry.equals(values)) {
            return true;
        }
        return false;
    }
}

The key is the #cleanup.register(value, newEntry, newEntry) in add: when any of the values gets garbage-collected, the newEntry gets removed from the #entries. It also unregisters the cleanup for the other values to prevent the FinalizationRegistry itself from holding onto the WeakEntry unnecessarily (until all its elements get garbage-collected, which may be never).

You can do the same with arbitrary tuples (arrays) instead of Sets, but you say that order within your "tuples" would not matter so they're sets. Make each WeakEntry an array containing WeakRefs then.