🎉 Add helpers for work with weak references and weak data structs

This commit is contained in:
Andrey Antukh
2025-08-25 14:18:32 +02:00
parent c1b2aa7628
commit 79786dde16
6 changed files with 249 additions and 35 deletions

View File

@@ -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?

View File

@@ -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))

View File

@@ -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)))))

View File

@@ -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<hash, Array<Entry>>
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 GCd
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;
}
}

View File

@@ -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 GCd, 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);
}
}

View File

@@ -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)))