diff --git a/common/src/app/common/types/fills/impl.cljc b/common/src/app/common/types/fills/impl.cljc index eca9d65156..30228a5a1c 100644 --- a/common/src/app/common/types/fills/impl.cljc +++ b/common/src/app/common/types/fills/impl.cljc @@ -7,7 +7,7 @@ (ns app.common.types.fills.impl (:require #?(:clj [clojure.data.json :as json]) - #?(:cljs [app.common.weak-map :as weak-map]) + #?(:cljs [app.common.weak :as weak]) [app.common.buffer :as buf] [app.common.data :as d] [app.common.data.macros :as dm] @@ -443,7 +443,7 @@ :code :invalid-fill :hint "found invalid fill on encoding fills to binary format"))))) - #?(:cljs (Fills. total dbuffer mbuffer image-ids (weak-map/create) nil) + #?(:cljs (Fills. total dbuffer mbuffer image-ids (weak/create-weak-value-map) nil) :clj (Fills. total dbuffer mbuffer nil)))))) (defn fills? diff --git a/common/src/app/common/types/path/impl.cljc b/common/src/app/common/types/path/impl.cljc index 0134e9abb1..1dfdf31593 100644 --- a/common/src/app/common/types/path/impl.cljc +++ b/common/src/app/common/types/path/impl.cljc @@ -12,7 +12,7 @@ (:require #?(:clj [app.common.fressian :as fres]) #?(:clj [clojure.data.json :as json]) - #?(:cljs [app.common.weak-map :as weak-map]) + #?(:cljs [app.common.weak :as weak]) [app.common.buffer :as buf] [app.common.data :as d] [app.common.data.macros :as dm] @@ -379,7 +379,7 @@ (-transform [this m] (let [buffer (buf/clone buffer)] (impl-transform buffer m size) - (PathData. size buffer (weak-map/create) nil))) + (PathData. size buffer (weak/create-weak-value-map) nil))) (-walk [_ f initial] (impl-walk buffer f initial size)) @@ -600,14 +600,14 @@ count (long (/ size SEGMENT-U8-SIZE))] (PathData. count (js/DataView. buffer) - (weak-map/create) + (weak/create-weak-value-map) nil)) (instance? js/DataView buffer) (let [buffer' (.-buffer ^js/DataView buffer) size (.-byteLength ^js/ArrayBuffer buffer') count (long (/ size SEGMENT-U8-SIZE))] - (PathData. count buffer (weak-map/create) nil)) + (PathData. count buffer (weak/create-weak-value-map) nil)) (instance? js/Uint8Array buffer) (from-bytes (.-buffer buffer)) diff --git a/common/src/app/common/weak.cljs b/common/src/app/common/weak.cljs new file mode 100644 index 0000000000..58ae9aad72 --- /dev/null +++ b/common/src/app/common/weak.cljs @@ -0,0 +1,59 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.weak + "A collection of helpers for work with weak references and weak + data structures on JS runtime." + (:refer-clojure :exclude [memoize]) + (:require + ["./weak/impl_weak_map.js" :as wm] + ["./weak/impl_weak_value_map.js" :as wvm])) + +(defn create-weak-value-map + [] + (new wvm/WeakValueMap.)) + +(defn create-weak-map + [] + (new wm/WeakEqMap #js {:hash hash :equals =})) + +(def ^:private state (new js/WeakMap)) +(def ^:private global-counter 0) + +(defn weak-key + "A simple helper that returns a stable key string for an object + while that object remains in memory and is not collected by the GC. + + Mainly used for assign temporal IDs/keys for react children + elements when the element has no specific id." + [o] + (let [key (.get ^js/WeakMap state o)] + (if (some? key) + key + (let [key (str "weak-key" (js* "~{}++" global-counter))] + (.set ^js/WeakMap state o key) + key)))) + +(defn memoize + "Returns a memoized version of a referentially transparent function. The + memoized version of the function keeps a cache of the mapping from arguments + to results and, when calls with the same arguments are repeated often, has + higher performance at the expense of higher memory use. + + The main difference with clojure.core/memoize, is that this function + uses weak-map, so cache is cleared once GC is passed and cached keys + are collected" + [f] + (let [mem (create-weak-map)] + (fn [& args] + (let [v (.get mem args)] + (if (undefined? v) + (let [ret (apply f args)] + (.set ^js mem args ret) + ret) + v))))) + + diff --git a/common/src/app/common/weak/impl_weak_map.js b/common/src/app/common/weak/impl_weak_map.js new file mode 100644 index 0000000000..2379ea7e14 --- /dev/null +++ b/common/src/app/common/weak/impl_weak_map.js @@ -0,0 +1,130 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright (c) KALEIDOS INC + */ +"use strict"; + +export class WeakEqMap { + constructor({ equals, hash }) { + this._equals = equals; + this._hash = hash; + + // buckets: Map> + this._buckets = new Map(); + + // Token -> (hash) so the FR cleanup can find & remove dead entries + // We store {hash, token} as heldValue for FinalizationRegistry + this._fr = new FinalizationRegistry(({ hash, token }) => { + const bucket = this._buckets.get(hash); + if (!bucket) return; + // Remove the entry whose token matches or whose key has been collected + let i = 0; + while (i < bucket.length) { + const e = bucket[i]; + const dead = e.keyRef.deref() === undefined; + if (dead || e.token === token) { + // swap-remove for O(1) + bucket[i] = bucket[bucket.length - 1]; + bucket.pop(); + continue; + } + i++; + } + if (bucket.length === 0) this._buckets.delete(hash); + }); + } + + _getBucket(hash) { + let b = this._buckets.get(hash); + if (!b) { + b = []; + this._buckets.set(hash, b); + } + return b; + } + + _findEntry(bucket, key) { + // Sweep dead entries opportunistically + let i = 0; + let found = null; + while (i < bucket.length) { + const e = bucket[i]; + const k = e.keyRef.deref(); + if (k === undefined) { + bucket[i] = bucket[bucket.length - 1]; + bucket.pop(); + continue; + } + if (found === null && this._equals(k, key)) { + found = e; + } + i++; + } + return found; + } + + set(key, value) { + if (key === null || (typeof key !== 'object' && typeof key !== 'function')) { + throw new TypeError('WeakEqMap keys must be objects (like WeakMap).'); + } + const hash = this._hash(key); + const bucket = this._getBucket(hash); + const existing = this._findEntry(bucket, key); + if (existing) { + existing.value = value; + return this; + } + const token = Object.create(null); // unique identity + const entry = { keyRef: new WeakRef(key), value, token }; + bucket.push(entry); + // Register for cleanup when key is GC’d + this._fr.register(key, { hash, token }, entry); + return this; + } + + get(key) { + const hash = this._hash(key); + const bucket = this._buckets.get(hash); + if (!bucket) return undefined; + const e = this._findEntry(bucket, key); + return e ? e.value : undefined; + } + + has(key) { + const hash = this._hash(key); + const bucket = this._buckets.get(hash); + if (!bucket) return false; + return !!this._findEntry(bucket, key); + } + + delete(key) { + const hash = this._hash(key); + const bucket = this._buckets.get(hash); + if (!bucket) return false; + let i = 0; + while (i < bucket.length) { + const e = bucket[i]; + const k = e.keyRef.deref(); + if (k === undefined) { + // clean dead + bucket[i] = bucket[bucket.length - 1]; + bucket.pop(); + continue; + } + if (this._equals(k, key)) { + // Unregister and remove + this._fr.unregister(e); // unregister via the registration "unregisterToken" = entry + bucket[i] = bucket[bucket.length - 1]; + bucket.pop(); + if (bucket.length === 0) this._buckets.delete(hash); + return true; + } + i++; + } + if (bucket.length === 0) this._buckets.delete(hash); + return false; + } +} diff --git a/common/src/app/common/weak/impl_weak_value_map.js b/common/src/app/common/weak/impl_weak_value_map.js new file mode 100644 index 0000000000..85497839d1 --- /dev/null +++ b/common/src/app/common/weak/impl_weak_value_map.js @@ -0,0 +1,54 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright (c) KALEIDOS INC + */ +"use strict"; + +export class WeakValueMap { + constructor() { + this._map = new Map(); // key -> {ref, token} + this._registry = new FinalizationRegistry((token) => { + this._map.delete(token.key); + }); + } + + set(key, value) { + const ref = new WeakRef(value); + const token = { key }; + this._map.set(key, { ref, token }); + this._registry.register(value, token, token); + return this; + } + + get(key) { + const entry = this._map.get(key); + if (!entry) return undefined; + const value = entry.ref.deref(); + if (value === undefined) { + // Value was GC’d, clean up + this._map.delete(key); + return undefined; + } + return value; + } + + has(key) { + const entry = this._map.get(key); + if (!entry) return false; + if (entry.ref.deref() === undefined) { + this._map.delete(key); + return false; + } + return true; + } + + delete(key) { + const entry = this._map.get(key); + if (!entry) return false; + this._registry.unregister(entry.token); + return this._map.delete(key); + } +} diff --git a/common/src/app/common/weak_map.cljs b/common/src/app/common/weak_map.cljs deleted file mode 100644 index ef204974e1..0000000000 --- a/common/src/app/common/weak_map.cljs +++ /dev/null @@ -1,29 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.common.weak-map - "A value based weak-map implementation (CLJS/JS)") - -(deftype ValueWeakMap [^js/Map data ^js/FinalizationRegistry registry] - Object - (clear [_] - (.clear data)) - (delete [_ key] - (.delete data key)) - (get [_ key] - (if-let [ref (.get data key)] - (.deref ^WeakRef ref) - nil)) - (set [_ key val] - (.set data key (js/WeakRef. val)) - (.register registry val key) - nil)) - -(defn create - [] - (let [data (js/Map.) - registry (js/FinalizationRegistry. #(.delete data %))] - (ValueWeakMap. data registry)))