Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,11 @@ class Identities(override val dynamicKind : IdKind) : IIdentities {
return RdIdUtil.mix(rdId, tail)
}

override fun mix(rdId: RdId, tail: Int): RdId {
fun mix(rdId: RdId, tail: Int): RdId {
return RdIdUtil.mix(rdId, tail)
}

override fun mix(rdId: RdId, tail: Long): RdId {
fun mix(rdId: RdId, tail: Long): RdId {
return RdIdUtil.mix(rdId, tail)
}
}
Expand Down Expand Up @@ -141,14 +141,12 @@ class SequentialIdentities(override val dynamicKind : IdKind) : IIdentities {
}

override fun mix(rdId: RdId, tail: String): RdId {
return RdId(StableMask or RdIdUtil.mix(rdId, tail).hash)
}

override fun mix(rdId: RdId, tail: Int): RdId {
return RdId(StableMask or RdIdUtil.mix(rdId, tail).hash)
}

override fun mix(rdId: RdId, tail: Long): RdId {
return RdId(StableMask or RdIdUtil.mix(rdId, tail).hash)
// Since dynamic RdIds are generated sequentially, the parent RdId often uses only a small number of bits (low entropy).
// Additionally, the tail string may be short, which can increase the risk of hash collisions.
// To improve hash distribution and reliability, we mix in extra data (tail length and a constant string) before mixing the tail itself.
var newRdId = RdIdUtil.mix(rdId, tail.length)
newRdId = RdIdUtil.mix(newRdId, "SequentialIdentities::Mix::RdId")
newRdId = RdIdUtil.mix(newRdId, tail)
return RdId(StableMask or newRdId.hash)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -173,16 +173,6 @@ interface IIdentities {
* Creates a stable identifier by mixing the parent ID with a string key.
*/
fun mix(rdId: RdId, tail: String): RdId

/**
* Creates a stable identifier by mixing the parent ID with an integer key.
*/
fun mix(rdId: RdId, tail: Int): RdId

/**
* Creates a stable identifier by mixing the parent ID with a long key.
*/
fun mix(rdId: RdId, tail: Long): RdId
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ class Protocol internal constructor(
val newExtension = create()
extensions[clazz] = newExtension
val declName = clazz.simpleName ?: error("Can't get simple name for class $clazz")
newExtension.identify(identity, identity.mix(RdId.Null, declName))
newExtension.identify(identity, identity.mix(RdId.Null, declName), true)
newExtension.bindTopLevel(lifetime, this, declName)
newExtension
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package com.jetbrains.rd.framework.base
import com.jetbrains.rd.framework.IIdentities
import com.jetbrains.rd.framework.IProtocol
import com.jetbrains.rd.framework.IRdDynamic
import com.jetbrains.rd.framework.Identities
import com.jetbrains.rd.framework.RdId
import com.jetbrains.rd.util.lifetime.Lifetime

Expand All @@ -26,35 +27,80 @@ interface IRdBindable : IRdDynamic {
fun bind()

/**
* Assigns IDs to this node and its child nodes in the graph.
* Assigns an [RdId] to this node and recursively to all its child nodes.
*
* @param identities the identity source used to generate child IDs.
* @param id the [RdId] to assign to this node.
* @param stable a recommendation for how child RdIds should be generated. Entities may override this value.
* - `true` — uses [IIdentities.mix] to produce hash-based, deterministic IDs derived from the parent ID
* and child name. Used when the same entity exists on both protocol sides and its children need
* matching IDs to find each other (e.g., extensions with built-in maps, sets, properties).
* Given the same parent ID, both sides will compute identical child IDs.
* - `false` — uses [IIdentities.next] to produce dynamic IDs.
* Used for entities created at runtime (e.g., items added to RdMap/RdList) where each side assigns
* its own IDs independently.
*
* Entities can override this parameter when their children require a specific strategy. For example,
* [RdExtBase] forces `stable = true` regardless of the incoming value, because its built-in children
* are part of a statically known structure that must match on both protocol sides. So even if an ext
* is encountered during dynamic (non-stable) identification, it will switch to stable IDs for its own subtree.
*/
fun identify(identities: IIdentities, id: RdId)
fun identify(identities: IIdentities, id: RdId, stable: Boolean)

/**
* Creates a clone of this IRdBindable not bound to any protocol
*/
fun deepClone() : IRdBindable = TODO("This is a base implementation of deepClone. Shouldn't be invoked. Introduced for AWS plugin to compile with Rider SDK 19.2.")
}

private fun computeChildRdId(identities: IIdentities, parent: RdId, stable: Boolean, i: Int): RdId {
return if (stable) {
if (identities is Identities) {
// for backward compatibility
identities.mix(parent, i)
} else {
identities.mix(parent, i.toString(2))
}
} else {
identities.next(parent)
}
}

//generator comprehension methods
fun <T:IRdBindable?> T.preBind(lf: Lifetime, parent: IRdDynamic, name: String) = this?.preBind(lf, parent, name)
fun <T:IRdBindable?> T.bind() = this?.bind()
fun <T:IRdBindable?> T.identify(identities: IIdentities, ids: RdId) = this?.identify(identities, ids)
fun <T:IRdBindable?> T.identify(identities: IIdentities, ids: RdId, stable: Boolean) = this?.identify(identities, ids, stable)

fun <T:IRdBindable?> Array<T>.identify(identities: IIdentities, ids: RdId) = forEachIndexed { i, v -> v?.identify(identities, identities.mix(ids, i))}
fun <T:IRdBindable?> Array<T>.identify(identities: IIdentities, ids: RdId, stable: Boolean) = forEachIndexed { i, v -> v?.identify(
identities,
computeChildRdId(identities, ids, stable, i),
stable
)}
fun <T:IRdBindable?> Array<T>.preBind(lf: Lifetime, parent: IRdDynamic, name: String) = forEachIndexed { i, v -> v?.preBind(lf,parent, "$name[$i]")}
fun <T:IRdBindable?> Array<T>.bind() = forEachIndexed { i, v -> v?.bind()}

fun <T:IRdBindable?> List<T>.identify(identities: IIdentities, ids: RdId) = forEachIndexed { i, v -> v?.identify(identities, identities.mix(ids, i))}
fun <T:IRdBindable?> List<T>.identify(identities: IIdentities, ids: RdId, stable: Boolean) = forEachIndexed { i, v -> v?.identify(
identities,
computeChildRdId(identities, ids, stable, i),
stable,
)}
fun <T:IRdBindable?> List<T>.preBind(lf: Lifetime, parent: IRdDynamic, name: String) = forEachIndexed { i, v -> v?.preBind(lf,parent, "$name[$i]")}
fun <T:IRdBindable?> List<T>.bind() = forEachIndexed { i, v -> v?.bind()}

internal fun Any?.identifyPolymorphic(identities: IIdentities, ids: RdId) {
internal fun Any?.identifyPolymorphic(identities: IIdentities, ids: RdId, stable: Boolean) {
if (this is IRdBindable) {
this.identify(identities, ids)
this.identify(identities, ids, stable)
} else {
(this as? Array<*>)?.forEachIndexed { i, v -> (v as? IRdBindable)?.identify(identities, identities.mix(ids, i))}
(this as? List<*>)?.forEachIndexed { i, v -> (v as? IRdBindable)?.identify(identities, identities.mix(ids, i))}
(this as? Array<*>)?.forEachIndexed { i, v -> (v as? IRdBindable)?.identify(
identities,
computeChildRdId(identities, ids, stable, i),
stable,
)}
(this as? List<*>)?.forEachIndexed { i, v -> (v as? IRdBindable)?.identify(
identities,
computeChildRdId(identities, ids, stable, i),
stable,
)}
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ abstract class RdBindableBase : IRdBindable, IPrintable {
val localBindLifetime = bindLifetime
if (localBindLifetime.isAlive) {
if (newExtension.rdid == RdId.Null)
newExtension.identify(proto.identity, proto.identity.mix(rdid, ".$name"))
newExtension.identify(proto.identity, proto.identity.mix(rdid, ".$name"), true)
newExtension.preBind(localBindLifetime, this, name)
newExtension.bind()
}
Expand Down Expand Up @@ -205,13 +205,21 @@ abstract class RdBindableBase : IRdBindable, IPrintable {
return child.findByRName(rName.dropNonEmptyRoot())
}

override fun identify(identities: IIdentities, id: RdId) {
override fun identify(identities: IIdentities, id: RdId, stable: Boolean) {
require(rdid.isNull) { "Already has RdId: $rdid, entity: $this" }
require(!id.isNull) { "Assigned RdId mustn't be null, entity: $this" }

rdid = id
for ((name, child) in bindableChildren) {
child?.identifyPolymorphic(identities, identities.mix(id, ".$name"))
child?.identifyPolymorphic(identities, computeChildRdId(identities, id, name, stable), stable)
}
}

private fun computeChildRdId(identities: IIdentities, parent: RdId, name: String, stable: Boolean): RdId {
return if (stable) {
identities.mix(parent, ".$name")
} else {
identities.next(parent)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,14 @@ abstract class RdExtBase : RdReactiveBase() {
})
}

override fun identify(
identities: IIdentities,
id: RdId,
stable: Boolean
) {
super.identify(identities, id, true) // enforce true to make stable ids for each built-in child
}

override fun assertBindingThread() = Unit

override fun onWireReceived(proto: IProtocol, buffer: AbstractBuffer, ctx: SerializationCtx, dispatchHelper: IRdWireableDispatchHelper) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ class AsyncRdMap<K : Any, V : Any> private constructor(
}
}

override fun identify(identities: IIdentities, id: RdId) {
override fun identify(identities: IIdentities, id: RdId, stable: Boolean) {
synchronized(map) {
map.identify(identities, id)
map.identify(identities, id, stable)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ class AsyncRdProperty<T>(val valueSerializer: ISerializer<T> = Polymorphic()) :
}
}

override fun identify(identities: IIdentities, id: RdId) {
override fun identify(identities: IIdentities, id: RdId, stable: Boolean) {
require(!id.isNull) { "Assigned RdId mustn't be null, entity: $this" }

synchronized (property) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ class AsyncRdSet<T : Any> private constructor(
}
}

override fun identify(identities: IIdentities, id: RdId) {
override fun identify(identities: IIdentities, id: RdId, stable: Boolean) {
synchronized(set) {
set.identify(identities, id)
set.identify(identities, id, stable)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ class InternRoot<TBase: Any>(val serializer: ISerializer<TBase> = Polymorphic())

}

override fun identify(identities: IIdentities, id: RdId) {
override fun identify(identities: IIdentities, id: RdId, stable: Boolean) {
require(rdid.isNull) { "Already has RdId: $rdid, entity: $this" }
require(!id.isNull) { "Assigned RdId mustn't be null, entity: $this" }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class RdList<V : Any> private constructor(val valSzr: ISerializer<V>, private va
for (index in 0 until size) {
val item = this[index]
if (item != null) {
item.identifyPolymorphic(proto.identity, proto.identity.next(rdid))
item.identifyPolymorphic(proto.identity, proto.identity.next(rdid), false)
definitions.add(tryPreBindValue(lifetime, item, index, false))
}
}
Expand Down Expand Up @@ -95,7 +95,7 @@ class RdList<V : Any> private constructor(val valSzr: ISerializer<V>, private va

val value = it.newValueOpt
if (it !is IViewableList.Event.Remove) {
value.identifyPolymorphic(proto.identity, proto.identity.next(rdid))
value.identifyPolymorphic(proto.identity, proto.identity.next(rdid), false)
definitions.add(it.index, tryPreBindValue(lifetime, value, it.index, false))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ open class RdMap<K : Any, V : Any> private constructor(

for ((key, value) in this) {
if (value != null) {
value.identifyPolymorphic(proto.identity, proto.identity.next(rdid))
value.identifyPolymorphic(proto.identity, proto.identity.next(rdid), false)
val definition = tryPreBindValue(lifetime, key, value, false)
if (definition != null)
definitions[key] = definition
Expand Down Expand Up @@ -96,7 +96,7 @@ open class RdMap<K : Any, V : Any> private constructor(

if (it !is IViewableMap.Event.Remove) {
val value = it.newValueOpt
value.identifyPolymorphic(proto.identity, proto.identity.next(rdid))
value.identifyPolymorphic(proto.identity, proto.identity.next(rdid), false)
val definition = tryPreBindValue(lifetime, it.key, value, false)
definitions.put(it.key, definition)?.terminate()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ abstract class RdPropertyBase<T>(val valueSerializer: ISerializer<T>) : RdReacti
// We need to terminate the current lifetime to unbind the existing value before assigning a new value, especially in cases where we are reassigning it.
bindDefinition.get()?.terminate()

v.identifyPolymorphic(proto.identity, proto.identity.next(rdid))
v.identifyPolymorphic(proto.identity, proto.identity.next(rdid), false)

val prevDefinition = bindDefinition.getAndSet(tryPreBindValue(lifetime, v, false))
prevDefinition?.terminate()
Expand Down Expand Up @@ -207,10 +207,10 @@ class RdOptionalProperty<T : Any>(valueSerializer: ISerializer<T> = Polymorphic(
}
}

override fun identify(identities: IIdentities, id: RdId) {
super.identify(identities, id)
override fun identify(identities: IIdentities, id: RdId, stable: Boolean) {
super.identify(identities, id, stable)
if (!optimizeNested)
valueOrNull?.identifyPolymorphic(identities, identities.next(id))
valueOrNull?.identifyPolymorphic(identities, identities.next(id), false)
}

override fun advise(lifetime: Lifetime, handler: (T) -> Unit) {
Expand Down Expand Up @@ -290,10 +290,10 @@ class RdProperty<T>(defaultValue: T, valueSerializer: ISerializer<T> = Polymorph
value = newValue
}

override fun identify(identities: IIdentities, id: RdId) {
super.identify(identities, id)
override fun identify(identities: IIdentities, id: RdId, stable: Boolean) {
super.identify(identities, id, stable)
if (!optimizeNested)
value?.identifyPolymorphic(identities, identities.next(id))
value?.identifyPolymorphic(identities, identities.next(id), false)
}

override fun advise(lifetime: Lifetime, handler: (T) -> Unit) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ class EndpointWiredRdTask<TReq, TRes>(
return@adviseOnce
}

taskResult.value.identifyPolymorphic(proto.identity, proto.identity.next(rdid))
taskResult.value.identifyPolymorphic(proto.identity, proto.identity.next(rdid), false)
lifetime.executeIfAlive {
taskResult.value.preBindPolymorphic(lifetime, call, rdid.toString())
if (lifetime.isNotAlive)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,29 +41,13 @@ class RdIdTest {
val serverIdentities = SequentialIdentities(IdKind.Server)

val testStrings = listOf("", "a", "test", "Protocol", "Extension", "InternRoot")
val testInts = listOf(0, 1, -1, Int.MAX_VALUE, Int.MIN_VALUE)
val testLongs = listOf(0L, 1L, -1L, Long.MAX_VALUE, Long.MIN_VALUE)

for (s in testStrings) {
val clientId = clientIdentities.mix(RdId.Null, s)
val serverId = serverIdentities.mix(RdId.Null, s)
assertTrue(clientId.hash and HIGH_BIT != 0L, "Client stable ID from string '$s' should have high bit set")
assertTrue(serverId.hash and HIGH_BIT != 0L, "Server stable ID from string '$s' should have high bit set")
}

for (i in testInts) {
val clientId = clientIdentities.mix(RdId.Null, i)
val serverId = serverIdentities.mix(RdId.Null, i)
assertTrue(clientId.hash and HIGH_BIT != 0L, "Client stable ID from int $i should have high bit set")
assertTrue(serverId.hash and HIGH_BIT != 0L, "Server stable ID from int $i should have high bit set")
}

for (l in testLongs) {
val clientId = clientIdentities.mix(RdId.Null, l)
val serverId = serverIdentities.mix(RdId.Null, l)
assertTrue(clientId.hash and HIGH_BIT != 0L, "Client stable ID from long $l should have high bit set")
assertTrue(serverId.hash and HIGH_BIT != 0L, "Server stable ID from long $l should have high bit set")
}
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ class RdSignalTest : RdFrameworkTestBase() {
_foo.bind()
}

override fun identify(identities: IIdentities, id: RdId) {
_foo.identify(identities, identities.mix(id, "foo"))
override fun identify(identities: IIdentities, id: RdId, stable: Boolean) {
_foo.identify(identities, identities.mix(id, "foo"), stable)
}


Expand Down Expand Up @@ -279,8 +279,8 @@ class RdSignalTest : RdFrameworkTestBase() {
_foo.bind()
}

override fun identify(identities: IIdentities, id: RdId) {
_foo.identify(identities, identities.mix(id, "foo"))
override fun identify(identities: IIdentities, id: RdId, stable: Boolean) {
_foo.identify(identities, identities.mix(id, "foo"), stable)
}

constructor() : this(RdSignal<Unit>())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ class RdPerContextMapTest : RdFrameworkTestBase() {
serverMap.bindTopLevel(serverLifetime, serverProtocol, "map")

key.updateValue(server1Cid).use {
serverProtocol.wire.send(serverProtocol.identity.mix(RdId.Null, 10)) {} // trigger key addition by protocol write
serverProtocol.wire.send(serverProtocol.identity.mix(RdId.Null, "10")) {} // trigger key addition by protocol write
}

assertTrue(serverProtocol.contexts.getValueSet(key).contains(server1Cid))
Expand Down
Loading
Loading