From 29b171680c98adfa00cf1b12bae1689bf4658bf2 Mon Sep 17 00:00:00 2001 From: Maxime Girardet Date: Sun, 04 Jan 2026 12:44:54 +0000 Subject: [PATCH] merge: Wrap the JVM & JS weak maps Closes #179 See merge request opensavvy/groundwork/pedestal!226 --- weak/src/jsTest/kotlin/Marker.kt | 17 +++++++++++++++++ weak/src/jsMain/kotlin/algorithms/JsWeakMap.kt | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ weak/src/jsTest/kotlin/algorithms/JsWeakMapTest.kt | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ weak/src/jvmMain/kotlin/algorithms/JvmWeakHashMap.kt | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 file(s) changed, 245 insertion(s)(+), 0 deletion(s)(-) diff --git a/weak/src/jsTest/kotlin/Marker.kt b/weak/src/jsTest/kotlin/Marker.kt new file mode 100644 --- /dev/null +++ b/weak/src/jsTest/kotlin/Marker.kt @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2025, OpenSavvy and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package opensavvy.pedestal.weak diff --git a/weak/src/jsMain/kotlin/algorithms/JsWeakMap.kt b/weak/src/jsMain/kotlin/algorithms/JsWeakMap.kt new file mode 100644 --- /dev/null +++ b/weak/src/jsMain/kotlin/algorithms/JsWeakMap.kt @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2025, OpenSavvy and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package opensavvy.pedestal.weak.algorithms + +import opensavvy.pedestal.weak.ExperimentalWeakApi +import opensavvy.pedestal.weak.WeakMap +import js.collections.WeakMap as JsWeakMap + +internal class JsWrappedWeakMap( + private val js: JsWeakMap, +) : WeakMap { + + override fun get(key: K): V? { + if (key == null) return null + + return js.get(key) + } + + override fun set(key: K, value: V) { + if (key == null) return + + js.set(key, value) + } + + @ExperimentalWeakApi + override fun contains(key: K): Boolean { + if (key == null) return false + + return js.has(key) + } + + @ExperimentalWeakApi + override fun remove(key: K): V? { + if (key == null) return null + + /* + * js.delete(key) does not return the value, it just returns true/false. + * We emulate returning the value by 'getting' it first. + * + * There is a chance that: + * 1. We get the value + * 2. The GC deletes the value + * 3. We call 'js.delete(key)', which returns false + * 4. Yet will still return the value. + * + * This doesn't really matter because the value is indeed removed after the call. + * This would look to users like the GC removed the value just before the call to 'remove' + * rather than during it. + * Since the GC is allowed to remove values at any point, this is fine. + */ + + val ret = js.get(key) + + js.delete(key) + + return ret + } +} + +/** + * Wraps a JS [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) into a multiplatform [WeakMap]. + * + * The resulting weak map has similar properties as the JS map. Most importantly: + * - The keys are held weakly. + * - The values are held strongly. + * + * ### Type restrictions + * + * The returned map can accept `null` as a key even though the JS map would throw an error. + * However, all mappings with a `null` key are ignored. That is, they behave as if they were + * immediately deleted by the GC. + * + * The underlying map doesn't support JS primitive types, like [String], [Int], [Double] and [Boolean]. + */ +@ExperimentalWeakApi +fun JsWeakMap.asMultiplatform(): WeakMap = + JsWrappedWeakMap(this) diff --git a/weak/src/jsTest/kotlin/algorithms/JsWeakMapTest.kt b/weak/src/jsTest/kotlin/algorithms/JsWeakMapTest.kt new file mode 100644 --- /dev/null +++ b/weak/src/jsTest/kotlin/algorithms/JsWeakMapTest.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2025, OpenSavvy and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalWeakApi::class) + +package opensavvy.pedestal.weak.algorithms + +import js.errors.TypeError +import opensavvy.pedestal.weak.ExperimentalWeakApi +import opensavvy.prepared.runner.testballoon.preparedSuite +import opensavvy.prepared.suite.assertions.checkThrows +import kotlin.js.json +import js.collections.WeakMap as JsWeakMap + +val JsWeakMapTest by preparedSuite { + + fun verify(value: T) { + val map = JsWrappedWeakMap(JsWeakMap()) + + map[value] = "5" + check(map[value] == "5") { "We just inserted the value $value in the map, it should still be there" } + check(value in map) { "We just inserted the value $value in the map, it should still be there" } + } + + test("Lists are allowed") { + verify(listOf(1, 2, 3)) + } + + test("Objects are allowed") { + verify(json("foo" to 1, "bar" to 2)) + } + + test("null is allowed but does not store the values") { + val map = JsWrappedWeakMap(JsWeakMap()) + + map[null] = "5" + check(map[null] == null) { "'null' isn't stored" } + check(null !in map) { "'null' isn't stored" } + } + + test("Strings are not allowed") { + checkThrows { + verify("foo") + } + } + + test("Int is not allowed") { + checkThrows { + verify(5) + } + } + + test("Double is not allowed") { + checkThrows { + verify(5.0) + } + } + + test("Booleans are not allowed") { + checkThrows { + verify(true) + } + + checkThrows { + verify(false) + } + } + +} diff --git a/weak/src/jvmMain/kotlin/algorithms/JvmWeakHashMap.kt b/weak/src/jvmMain/kotlin/algorithms/JvmWeakHashMap.kt new file mode 100644 --- /dev/null +++ b/weak/src/jvmMain/kotlin/algorithms/JvmWeakHashMap.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2025, OpenSavvy and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package opensavvy.pedestal.weak.algorithms + +import opensavvy.pedestal.weak.ExperimentalWeakApi +import opensavvy.pedestal.weak.WeakMap +import java.util.* + +private class JvmWeakHashMap( + private val java: WeakHashMap, +) : WeakMap { + + override fun get(key: K): V? = + java[key] + + override fun set(key: K, value: V) { + java[key] = value + } + + @ExperimentalWeakApi + override fun contains(key: K): Boolean = + key in java + + @ExperimentalWeakApi + override fun remove(key: K): V? = + java.remove(key) + +} + +/** + * Wraps a JVM [WeakHashMap] into a multiplatform [WeakMap]. + * + * The resulting weak map has the exact same properties as the JVM map. + * Most importantly: + * - The keys are held weakly. + * - The values are held strongly. + * - The map is not thread-safe. + */ +@ExperimentalWeakApi +fun WeakHashMap.asMultiplatform(): WeakMap = + JvmWeakHashMap(this) -- tangled.sh