mirror of
https://github.com/penpot/penpot.git
synced 2025-12-12 06:24:17 +01:00
🎉 Add helpers for work with weak references and weak data structs
This commit is contained in:
@@ -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?
|
||||
|
||||
@@ -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))
|
||||
|
||||
59
common/src/app/common/weak.cljs
Normal file
59
common/src/app/common/weak.cljs
Normal 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)))))
|
||||
|
||||
|
||||
130
common/src/app/common/weak/impl_weak_map.js
Normal file
130
common/src/app/common/weak/impl_weak_map.js
Normal 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 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;
|
||||
}
|
||||
}
|
||||
54
common/src/app/common/weak/impl_weak_value_map.js
Normal file
54
common/src/app/common/weak/impl_weak_value_map.js
Normal 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 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);
|
||||
}
|
||||
}
|
||||
@@ -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)))
|
||||
Reference in New Issue
Block a user