|
| 1 | +/* global globalThis */ |
| 2 | +import { Far, isObject } from '@endo/marshal'; |
| 3 | + |
| 4 | +// @ts-check |
| 5 | +const { WeakRef, FinalizationRegistry } = globalThis; |
| 6 | + |
| 7 | +/** |
| 8 | + * @template K |
| 9 | + * @template {object} V |
| 10 | + * @typedef {Pick<Map<K, V>, 'get' | 'has' | 'delete'> & |
| 11 | + * { |
| 12 | + * set: (key: K, value: V) => void, |
| 13 | + * clearWithoutFinalizing: () => void, |
| 14 | + * getSize: () => number, |
| 15 | + * }} FinalizingMap |
| 16 | + */ |
| 17 | + |
| 18 | +/** |
| 19 | + * |
| 20 | + * Elsewhere this is known as a "Weak Value Map". Whereas a std JS WeakMap |
| 21 | + * is weak on its keys, this map is weak on its values. It does not retain these |
| 22 | + * values strongly. If a given value disappears, then the entries for it |
| 23 | + * disappear from every weak-value-map that holds it as a value. |
| 24 | + * |
| 25 | + * Just as a WeakMap only allows gc-able values as keys, a weak-value-map |
| 26 | + * only allows gc-able values as values. |
| 27 | + * |
| 28 | + * Unlike a WeakMap, a weak-value-map unavoidably exposes the non-determinism of |
| 29 | + * gc to its clients. Thus, both the ability to create one, as well as each |
| 30 | + * created one, must be treated as dangerous capabilities that must be closely |
| 31 | + * held. A program with access to these can read side channels though gc that do |
| 32 | + * not* rely on the ability to measure duration. This is a separate, and bad, |
| 33 | + * timing-independent side channel. |
| 34 | + * |
| 35 | + * This non-determinism also enables code to escape deterministic replay. In a |
| 36 | + * blockchain context, this could cause validators to differ from each other, |
| 37 | + * preventing consensus, and thus preventing chain progress. |
| 38 | + * |
| 39 | + * JS standards weakrefs have been carefully designed so that operations which |
| 40 | + * `deref()` a weakref cause that weakref to remain stable for the remainder of |
| 41 | + * that turn. The operations below guaranteed to do this derefing are `has`, |
| 42 | + * `get`, `set`, `delete`. Note that neither `clearWithoutFinalizing` nor |
| 43 | + * `getSize` are guaranteed to deref. Thus, a call to `map.getSize()` may |
| 44 | + * reflect values that might still be collected later in the same turn. |
| 45 | + * |
| 46 | + * @template K |
| 47 | + * @template {object} V |
| 48 | + * @param {(key: K) => void} [finalizer] |
| 49 | + * @returns {FinalizingMap<K, V> & |
| 50 | + * import('@endo/eventual-send').RemotableBrand<{}, FinalizingMap<K, V>> |
| 51 | + * } |
| 52 | + */ |
| 53 | +export const makeFinalizingMap = finalizer => { |
| 54 | + if (!WeakRef || !FinalizationRegistry) { |
| 55 | + /** @type Map<K, V> */ |
| 56 | + const keyToVal = new Map(); |
| 57 | + return Far('fakeFinalizingMap', { |
| 58 | + clearWithoutFinalizing: keyToVal.clear.bind(keyToVal), |
| 59 | + get: keyToVal.get.bind(keyToVal), |
| 60 | + has: keyToVal.has.bind(keyToVal), |
| 61 | + set: (key, val) => { |
| 62 | + keyToVal.set(key, val); |
| 63 | + }, |
| 64 | + delete: keyToVal.delete.bind(keyToVal), |
| 65 | + getSize: () => keyToVal.size, |
| 66 | + }); |
| 67 | + } |
| 68 | + /** @type Map<K, WeakRef<any>> */ |
| 69 | + const keyToRef = new Map(); |
| 70 | + const registry = new FinalizationRegistry(key => { |
| 71 | + // Because this will delete the current binding of `key`, we need to |
| 72 | + // be sure that it is not called because a previous binding was collected. |
| 73 | + // We do this with the `unregister` in `set` below, assuming that |
| 74 | + // `unregister` *immediately* suppresses the finalization of the thing |
| 75 | + // it unregisters. TODO If this is not actually guaranteed, i.e., if |
| 76 | + // finalizations that have, say, already been scheduled might still |
| 77 | + // happen after they've been unregistered, we will need to revisit this. |
| 78 | + // eslint-disable-next-line no-use-before-define |
| 79 | + finalizingMap.delete(key); |
| 80 | + }); |
| 81 | + const finalizingMap = Far('finalizingMap', { |
| 82 | + /** |
| 83 | + * `clearWithoutFinalizing` does not `deref` anything, and so does not |
| 84 | + * suppress collection of the weakly-pointed-to values until the end of the |
| 85 | + * turn. Because `clearWithoutFinalizing` immediately removes all entries |
| 86 | + * from this map, this possible collection is not observable using only this |
| 87 | + * map instance. But it is observable via other uses of WeakRef or |
| 88 | + * FinalizationGroup, including other map instances made by this |
| 89 | + * `makeFinalizingMap`. |
| 90 | + */ |
| 91 | + clearWithoutFinalizing: () => { |
| 92 | + for (const ref of keyToRef.values()) { |
| 93 | + registry.unregister(ref); |
| 94 | + } |
| 95 | + keyToRef.clear(); |
| 96 | + }, |
| 97 | + // Does deref, and thus does guarantee stability of the value until the |
| 98 | + // end of the turn. |
| 99 | + get: key => keyToRef.get(key)?.deref(), |
| 100 | + has: key => finalizingMap.get(key) !== undefined, |
| 101 | + // Does deref, and thus does guarantee stability of both old and new values |
| 102 | + // until the end of the turn. |
| 103 | + set: (key, ref) => { |
| 104 | + assert(isObject(ref)); |
| 105 | + finalizingMap.delete(key); |
| 106 | + const newWR = new WeakRef(ref); |
| 107 | + keyToRef.set(key, newWR); |
| 108 | + registry.register(ref, key, newWR); |
| 109 | + }, |
| 110 | + delete: key => { |
| 111 | + const wr = keyToRef.get(key); |
| 112 | + if (!wr) { |
| 113 | + return false; |
| 114 | + } |
| 115 | + |
| 116 | + registry.unregister(wr); |
| 117 | + keyToRef.delete(key); |
| 118 | + |
| 119 | + // Our semantics are to finalize upon explicit `delete`, `set` (which |
| 120 | + // calls `delete`) or garbage collection (which also calls `delete`). |
| 121 | + // `clearWithoutFinalizing` is exempt. |
| 122 | + if (finalizer) { |
| 123 | + finalizer(key); |
| 124 | + } |
| 125 | + return true; |
| 126 | + }, |
| 127 | + getSize: () => keyToRef.size, |
| 128 | + }); |
| 129 | + return finalizingMap; |
| 130 | +}; |
0 commit comments