Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add signal commands #20876

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion signals/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
Expand Down
112 changes: 112 additions & 0 deletions signals/src/main/java/com/vaadin/signals/Id.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright 2000-2025 Vaadin Ltd.
*
* 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 com.vaadin.signals;

import java.math.BigInteger;
import java.util.Base64;
import java.util.Base64.Encoder;
import java.util.concurrent.ThreadLocalRandom;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;

/**
* Generated identifier for signals and other related resources.
* <p>
* The id is a random 64-bit number to be more compact than a full 128-bit UUID
* or such. The ids don't need to be globally unique but only unique within a
* smaller context so the risk of collisions is still negligible. The value is
* JSON serialized as a base64-encoded string with a special case,
* <code>""</code>, for the frequently used special 0 id. The ids are comparable
* to facilitate consistent ordering to avoid deadlocks in certain situations.
*
* @param value
* the id value as a 64-bit integer
*/
public record Id(long value) implements Comparable<Id> {
/**
* Default or initial id in various contexts. Always used for the root node
* in a signal hierarchy. The zero id is frequently used and has a custom
* compact JSON representation.
*/
public static final Id ZERO = new Id(0);

/**
* Special id value reserved for internal bookkeeping.
*/
public static final Id MAX = new Id(Long.MAX_VALUE);

/*
* Padding refers to the trailing = characters that are only necessary when
* base64 values are concatenated together
*/
private static final Encoder base64Encoder = Base64.getEncoder()
.withoutPadding();

/**
* Creates a random id. Randomness is only needed to reduce the risk of
* collisions but there's no security impact from being able to guess random
* ids.
*
* @return a random id, not <code>null</code>
*/
public static Id random() {
var random = ThreadLocalRandom.current();

long value;
do {
value = random.nextLong();
} while (value == 0 || value == Long.MAX_VALUE);

return new Id(value);
}

/**
* Parses the given base64 string as an id. As a special case, the empty
* string is parsed as {@link #ZERO}.
*
* @param base64
* the base64 string to parse, not <code>null</code>
* @return the parsed id.
*/
@JsonCreator
public static Id parse(String base64) {
if (base64.equals("")) {
return ZERO;
}
byte[] bytes = Base64.getDecoder().decode(base64);
return new Id(new BigInteger(bytes).longValue());
}

/**
* Returns this id value as a base64 string.
*
* @return the base64 string representing this id
*/
@JsonValue
public final String asBase64() {
if (value == 0) {
return "";
}
byte[] bytes = BigInteger.valueOf(value).toByteArray();
return base64Encoder.encodeToString(bytes);
}

@Override
public int compareTo(Id other) {
return Long.compare(value, other.value);
}
}
64 changes: 64 additions & 0 deletions signals/src/main/java/com/vaadin/signals/ListSignal.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2000-2025 Vaadin Ltd.
*
* 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 com.vaadin.signals;

/**
* The rest of this class will be implemented later.
*/
public class ListSignal {

/**
* A list insertion position before and/or after the referenced entries. If
* both entries are defined, then this position represents an exact match
* that is valid only if the two entries are adjacent. If only one is
* defined, then the position is relative to only that position. A position
* with neither reference is not valid for inserts but it is valid to test a
* parent-child relationship regardless of the child position.
* {@link Id#ZERO} represents the edge of the list, i.e. the first or the
* last position.
*
* @param after
* id of the node to insert immediately after, nor
* <code>null</code> to not define a constraint
* @param before
* id of the node to insert immediately before, nor
* <code>null</code> to not define a constraint
Comment on lines +33 to +38
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't these two be or instead:

Suggested change
* @param after
* id of the node to insert immediately after, nor
* <code>null</code> to not define a constraint
* @param before
* id of the node to insert immediately before, nor
* <code>null</code> to not define a constraint
* @param after
* id of the node to insert immediately after, or
* <code>null</code> to not define a constraint
* @param before
* id of the node to insert immediately before, or
* <code>null</code> to not define a constraint

*/
public record ListPosition(Id after, Id before) {
/**
* Gets the insertion position that corresponds to the beginning of the
* list.
*
* @return a list position for the beginning of the list, not
* <code>null</code>
*/
public static ListPosition first() {
// After edge
return new ListPosition(Id.ZERO, null);
}

/**
* Gets the insertion position that corresponds to the end of the list.
*
* @return a list position for the end of the list, not
* <code>null</code>
*/
public static ListPosition last() {
// Before edge
return new ListPosition(null, Id.ZERO);
}
Comment on lines +41 to +62
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I easily understand this part:
The description of the after param says: id of the node to insert immediately after...
and description of the before param says: id of the node to insert immediately before...

What is the role of the ZERO? Is the ZERO an always existing root pointer that will store the head position (or the head == tail == ZERO in case of an empty list)?

If so, calling first() returns the before first (after the ZERO) position which makes sense. But, the last() returns before the ZERO, which I don't simply get :)

Probably, something in the implementation is being optimized by this(?), but this representation seems a bit confusing.

}
}
116 changes: 116 additions & 0 deletions signals/src/main/java/com/vaadin/signals/Node.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* Copyright 2000-2025 Vaadin Ltd.
*
* 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 com.vaadin.signals;

import java.util.List;
import java.util.Map;
import java.util.Objects;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.NullNode;

/**
* A node in a signal tree. Each node represents as signal entry. Nodes are
* immutable and referenced by an {@link Id} rather than directly referencing
* the node instance. The node is either a {@link Data} node carrying actual
* signal data or an {@link Alias} node that allows multiple signal ids to
* reference the same data.
*/
public sealed interface Node {

/**
* An empty data node without parent, scope owner, value or children and the
* initial last update id.
*/
public static final Data EMPTY = new Data(null, Id.ZERO, null, null,
List.of(), Map.of());

/**
* A node alias. An alias node allows multiple signal ids to reference the
* same data.
*
* @param target
* the id of the alias target, not <code>null</code>
*/
public record Alias(Id target) implements Node {
}

/**
* A data node. The node represents the actual data behind a signal
* instance.
*
* @param parent
* the parent id, or <code>null</code> for the root node
* @param lastUpdate
* a unique id for the update that last updated this data node,
* not <code>null</code>
* @param scopeOwner
* the id of the external owner of this node, or
* <code>null</code> if the node has no owner. Any node with an
* owner is deleted if the owner is disconnected.
* @param value
* the JSON value of this node, or <code>null</code> if there is
* no value
* @param listChildren
* a list of child ids, or the an list if the node has no list
* children
Comment on lines +67 to +69
Copy link
Contributor

@taefi taefi Jan 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems the sentence misses an empty before the list:

Suggested change
* @param listChildren
* a list of child ids, or the an list if the node has no list
* children
* @param listChildren
* a list of child ids, or an empty list if the node has no list
* children

* @param mapChildren
* a sequenced map from key to child id, or an empty map if the
* node has no map children
*/
public record Data(Id parent, Id lastUpdate, Id scopeOwner, JsonNode value,
List<Id> listChildren,
Map<String, Id> mapChildren) implements Node {
/**
* Creates a new data node.
*
* @param parent
* the parent id, or <code>null</code> for the root node
* @param lastUpdate
* a unique id for the update that last updated this data
* node, not <code>null</code>
* @param scopeOwner
* the id of the external owner of this node, or
* <code>null</code> if the node has no owner. Any node with
* an owner is deleted if the owner is disconnected.
* @param value
* the JSON value of this node, or <code>null</code> if there
* is no value
* @param listChildren
* a list of child ids, or the an list if the node has no
* list children
Comment on lines +92 to +94
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. Though, I prefer to get rid of this almost duplicate block.

* @param mapChildren
* a sequenced map from key to child id, or an empty map if
* the node has no map children
*/
Comment on lines +77 to +98
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this in case of a compact constructor?
I suggest keeping only the Creates a new data node. sentence, and combine it with the next block of javadoc.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has no practical purpose other than to stop the checker from complaining about missing @param values for the implicit method parameters.

/*
* There's no point in copying the record components here since they are
* already documented on the top level, but the Javadoc checker insist
* that this constructor also has full documentation...
*/
public Data {
Objects.requireNonNull(lastUpdate);

/*
* Avoid accidentally making a distinction between the two different
* nulls that will look the same after JSON deserialization
*/
if (value instanceof NullNode) {
value = null;
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
}

Loading
Loading