Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.vaadin.collaborationengine.Topic Maven / Gradle / Ivy
/*
* Copyright 2020-2022 Vaadin Ltd.
*
* This program is available under Commercial Vaadin Runtime License 1.0
* (CVRLv1).
*
* For the full License, see http://vaadin.com/license/cvrl-1
*/
package com.vaadin.collaborationengine;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.vaadin.collaborationengine.EntryList.ListEntrySnapshot;
import com.vaadin.collaborationengine.MembershipEvent.MembershipEventType;
import com.vaadin.flow.function.SerializableBiConsumer;
import com.vaadin.flow.function.SerializableConsumer;
import com.vaadin.flow.shared.Registration;
class Topic {
enum ChangeResult {
ACCEPTED, REJECTED;
}
/**
* Marker interface to have something more specific than Object in method
* signatures.
*/
interface ChangeDetails {
// Marker interface
}
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
static class Entry {
final UUID revisionId;
final JsonNode data;
final UUID scopeOwnerId;
@JsonCreator
public Entry(@JsonProperty("id") UUID id,
@JsonProperty("data") JsonNode data,
@JsonProperty("scopeOwnerId") UUID scopeOwnerId) {
this.revisionId = id;
this.data = data;
this.scopeOwnerId = scopeOwnerId;
}
}
static class Snapshot {
private static final TypeReference> LISTS_TYPE = new TypeReference<>() {
};
private static final TypeReference>> MAPS_TYPE = new TypeReference<>() {
};
private static final TypeReference> TIMEOUTS_TYPE = new TypeReference<>() {
};
private static final TypeReference> NODES_TYPE = new TypeReference<>() {
};
private static final String LATEST = "latest";
private static final String LISTS = "lists";
private static final String MAPS = "maps";
private static final String LIST_TIMEOUTS = "list-timeouts";
private static final String MAP_TIMEOUTS = "map-timeouts";
private static final String ACTIVE_NODES = "active-nodes";
private static final String BACKEND_NODES = "backend-nodes";
private final ObjectNode objectNode;
Snapshot(ObjectNode objectNode) {
this.objectNode = Objects.requireNonNull(objectNode);
}
static Snapshot fromTopic(Topic topic, UUID latestChangeId) {
ObjectNode objectNode = JsonUtil.getObjectMapper()
.createObjectNode();
objectNode.put(LATEST, latestChangeId.toString());
objectNode.set(LISTS, JsonUtil.toJsonNode(topic.namedListData));
objectNode.set(MAPS, JsonUtil.toJsonNode(topic.namedMapData));
objectNode.set(LIST_TIMEOUTS,
JsonUtil.toJsonNode(topic.listExpirationTimeouts));
objectNode.set(MAP_TIMEOUTS,
JsonUtil.toJsonNode(topic.mapExpirationTimeouts));
objectNode.set(ACTIVE_NODES,
JsonUtil.toJsonNode(topic.activeNodes));
objectNode.set(BACKEND_NODES,
JsonUtil.toJsonNode(topic.backendNodes));
return new Snapshot(objectNode);
}
ObjectNode toObjectNode() {
return objectNode;
}
Map getLists() {
return JsonUtil.toInstance(objectNode.get(LISTS), LISTS_TYPE);
}
Map> getMaps() {
return JsonUtil.toInstance(objectNode.get(MAPS), MAPS_TYPE);
}
Map getListTimeouts() {
return JsonUtil.toInstance(objectNode.get(LIST_TIMEOUTS),
TIMEOUTS_TYPE);
}
Map getMapTimeouts() {
return JsonUtil.toInstance(objectNode.get(MAP_TIMEOUTS),
TIMEOUTS_TYPE);
}
List getActiveNodes() {
return JsonUtil.toInstance(objectNode.get(ACTIVE_NODES),
NODES_TYPE);
}
List getBackendNodes() {
return JsonUtil.toInstance(objectNode.get(BACKEND_NODES),
NODES_TYPE);
}
}
private final String id;
private final CollaborationEngine collaborationEngine;
private final Map> namedMapData = new HashMap<>();
private final Map namedListData = new HashMap<>();
final Map mapExpirationTimeouts = new HashMap<>();
final Map listExpirationTimeouts = new HashMap<>();
private final List activeNodes = new ArrayList<>();
private Instant lastDisconnected;
private final List> changeListeners = new ArrayList<>();
private final Map> changeResultTrackers = new ConcurrentHashMap<>();
private final List backendNodes = new ArrayList<>();
private final Backend.EventLog eventLog;
private UUID lastSnapshotId;
private boolean leader;
private int changeCount;
Topic(String id, CollaborationEngine collaborationEngine,
Backend.EventLog eventLog) {
this.id = id;
this.collaborationEngine = collaborationEngine;
this.eventLog = eventLog;
final Backend backend = getBackend();
backend.addMembershipListener(event -> {
if (event.getType().equals(MembershipEventType.LEAVE)) {
handleNodeLeave(event.getNodeId());
}
});
if (eventLog != null) {
BackendUtil
.initializeFromSnapshot(collaborationEngine,
this::initializeFromSnapshot)
.thenAccept(uuid -> lastSnapshotId = uuid);
}
}
UUID getCurrentNodeId() {
return getBackend().getNodeId();
}
private Backend getBackend() {
return collaborationEngine.getConfiguration().getBackend();
}
private CompletableFuture initializeFromSnapshot() {
return getBackend().loadLatestSnapshot(id)
.thenCompose(this::loadAndSubscribe);
}
private CompletableFuture loadAndSubscribe(
Backend.Snapshot snapshot) {
CompletableFuture future = new CompletableFuture<>();
try {
UUID latestChange = null;
if (snapshot != null) {
ObjectNode payload = JsonUtil.fromString(snapshot.getPayload());
latestChange = JsonUtil.toUUID(payload.get(Snapshot.LATEST));
loadSnapshot(new Snapshot(payload));
eventLog.subscribe(latestChange, this::applyChange);
} else {
eventLog.subscribe(null, this::applyChange);
}
ObjectNode nodeEvent = JsonUtil.createNodeJoin(getCurrentNodeId());
eventLog.submitEvent(UUID.randomUUID(),
JsonUtil.toString(nodeEvent));
future.complete(latestChange);
} catch (Backend.EventIdNotFoundException e) {
future.completeExceptionally(e);
}
return future;
}
synchronized void handleNodeLeave(UUID nodeId) {
Backend backend = this.collaborationEngine.getConfiguration()
.getBackend();
backendNodes.remove(nodeId);
if (!backendNodes.isEmpty()
&& backendNodes.get(0).equals(backend.getNodeId())) {
becomeLeader();
}
if (leader) {
cleanupStaleEntries(nodeId::equals);
}
}
private void cleanupStaleEntries(Predicate isStale) {
namedMapData.entrySet().stream()
.flatMap(map -> map.getValue().entrySet().stream().filter(
entry -> isStale.test(entry.getValue().scopeOwnerId))
.map(entry -> {
ObjectNode change = JsonUtil.createPutChange(
map.getKey(), entry.getKey(), null, null,
null);
change.put(JsonUtil.CHANGE_EXPECTED_ID,
entry.getValue().revisionId.toString());
return change;
}))
.collect(Collectors.toList())
.forEach(change -> eventLog.submitEvent(UUID.randomUUID(),
JsonUtil.toString(change)));
namedListData.entrySet().stream()
.flatMap(list -> list.getValue().stream()
.filter(entry -> isStale.test(entry.scopeOwnerId))
.map(entry -> {
ObjectNode change = JsonUtil.createListChange(
ListOperation.OperationType.SET,
list.getKey(), entry.id.toString(), null,
null, null, Collections.emptyMap(),
Collections.emptyMap(), null);
change.put(JsonUtil.CHANGE_EXPECTED_ID,
entry.revisionId.toString());
return change;
}))
.collect(Collectors.toList())
.forEach(change -> eventLog.submitEvent(UUID.randomUUID(),
JsonUtil.toString(change)));
}
Registration subscribeToChange(
SerializableBiConsumer changeListener) {
clearExpiredData();
changeListeners.add(changeListener);
return () -> changeListeners.remove(changeListener);
}
private void clearExpiredData() {
Clock clock = collaborationEngine.getClock();
if (isLeader() && lastDisconnected != null) {
Instant now = clock.instant();
mapExpirationTimeouts.entrySet().stream()
.filter(entry -> now
.isAfter(lastDisconnected.plus(entry.getValue()))
&& namedMapData.containsKey(entry.getKey()))
.map(Map.Entry::getKey).collect(Collectors.toList())
.forEach(name -> {
namedMapData.get(name).entrySet().stream()
.map(entry -> {
ObjectNode change = JsonUtil
.createPutChange(name,
entry.getKey(), null, null,
null);
change.put(JsonUtil.CHANGE_EXPECTED_ID,
entry.getValue().revisionId
.toString());
return change;
}).collect(Collectors.toList())
.forEach(change -> eventLog.submitEvent(
UUID.randomUUID(),
JsonUtil.toString(change)));
mapExpirationTimeouts.remove(name);
});
listExpirationTimeouts.entrySet().stream()
.filter(entry -> now
.isAfter(lastDisconnected.plus(entry.getValue()))
&& namedListData.containsKey(entry.getKey()))
.map(Map.Entry::getKey).collect(Collectors.toList())
.forEach(name -> {
namedListData.get(name).stream().map(entry -> {
ObjectNode change = JsonUtil.createListChange(
ListOperation.OperationType.SET, name,
entry.id.toString(), null, null, null,
Collections.emptyMap(),
Collections.emptyMap(), null);
change.put(JsonUtil.CHANGE_EXPECTED_ID,
entry.revisionId.toString());
return change;
}).collect(Collectors.toList())
.forEach(change -> eventLog.submitEvent(
UUID.randomUUID(),
JsonUtil.toString(change)));
listExpirationTimeouts.remove(name);
});
}
}
Stream getMapData(String mapName) {
Map mapData = namedMapData.get(mapName);
if (mapData == null) {
return Stream.empty();
}
return mapData.entrySet().stream()
.map(entry -> new MapChange(mapName, MapChangeType.PUT,
entry.getKey(), null, entry.getValue().data, null,
entry.getValue().revisionId));
}
JsonNode getMapValue(String mapName, String key) {
Map map = namedMapData.get(mapName);
if (map == null || !map.containsKey(key)) {
return null;
}
return map.get(key).data.deepCopy();
}
synchronized ChangeResult applyChange(UUID trackingId, String payload) {
ObjectNode change = JsonUtil.fromString(payload);
changeCount++;
String type = change.get(JsonUtil.CHANGE_TYPE).asText();
ChangeDetails details;
switch (type) {
case JsonUtil.CHANGE_TYPE_PUT:
details = applyMapPut(trackingId, change);
break;
case JsonUtil.CHANGE_TYPE_REPLACE:
details = applyMapReplace(trackingId, change);
break;
case JsonUtil.CHANGE_TYPE_INSERT_BEFORE:
details = applyListInsert(trackingId, change, true);
break;
case JsonUtil.CHANGE_TYPE_INSERT_AFTER:
details = applyListInsert(trackingId, change, false);
break;
case JsonUtil.CHANGE_TYPE_MOVE_BEFORE:
details = applyListMove(trackingId, change, true);
break;
case JsonUtil.CHANGE_TYPE_MOVE_AFTER:
details = applyListMove(trackingId, change, false);
break;
case JsonUtil.CHANGE_TYPE_LIST_SET:
details = applyListSet(trackingId, change);
break;
case JsonUtil.CHANGE_TYPE_MAP_TIMEOUT:
applyMapTimeout(change);
return ChangeResult.ACCEPTED;
case JsonUtil.CHANGE_TYPE_LIST_TIMEOUT:
applyListTimeout(change);
return ChangeResult.ACCEPTED;
case JsonUtil.CHANGE_NODE_ACTIVATE: {
UUID nodeId = UUID
.fromString(change.get(JsonUtil.CHANGE_NODE_ID).asText());
activeNodes.add(nodeId);
lastDisconnected = null;
return ChangeResult.ACCEPTED;
}
case JsonUtil.CHANGE_NODE_DEACTIVATE: {
UUID nodeId = UUID
.fromString(change.get(JsonUtil.CHANGE_NODE_ID).asText());
activeNodes.remove(nodeId);
if (activeNodes.isEmpty()) {
lastDisconnected = collaborationEngine.getClock().instant();
}
return ChangeResult.ACCEPTED;
}
case JsonUtil.CHANGE_NODE_JOIN: {
UUID nodeId = UUID
.fromString(change.get(JsonUtil.CHANGE_NODE_ID).asText());
if (backendNodes.isEmpty() && collaborationEngine.getConfiguration()
.getBackend().getNodeId().equals(nodeId)) {
becomeLeader();
}
backendNodes.add(nodeId);
return ChangeResult.ACCEPTED;
}
default:
throw new UnsupportedOperationException(
"Type '" + type + "' is not a supported change type");
}
ChangeResult result = details != null ? ChangeResult.ACCEPTED
: ChangeResult.REJECTED;
SerializableConsumer changeResultTracker = changeResultTrackers
.remove(trackingId);
if (changeResultTracker != null) {
changeResultTracker.accept(result);
}
if (ChangeResult.ACCEPTED.equals(result)) {
EventUtil.fireEvents(changeListeners,
listener -> listener.accept(trackingId, details), true);
}
if (lastSnapshotId == null) {
UUID newId = UUID.randomUUID();
String snapshot = JsonUtil.toString(
Topic.Snapshot.fromTopic(this, trackingId).toObjectNode());
getBackend().replaceSnapshot(id, null, newId, snapshot);
getBackend().loadLatestSnapshot(id)
.thenAccept(s -> lastSnapshotId = s.getId());
}
if (leader && changeCount % 100 == 0) {
UUID newId = UUID.randomUUID();
ObjectNode snapshot = Topic.Snapshot.fromTopic(this, trackingId)
.toObjectNode();
getBackend()
.replaceSnapshot(id, lastSnapshotId, newId,
JsonUtil.toString(snapshot))
.thenAccept(s -> eventLog.truncate(lastSnapshotId));
lastSnapshotId = newId;
}
return result;
}
void loadSnapshot(Snapshot snapshot) {
if (!namedListData.isEmpty() || !namedMapData.isEmpty()
|| !backendNodes.isEmpty()) {
throw new IllegalStateException(
"You can only load snapshots for empty topics");
}
namedListData.putAll(snapshot.getLists());
namedMapData.putAll(snapshot.getMaps());
listExpirationTimeouts.putAll(snapshot.getListTimeouts());
mapExpirationTimeouts.putAll(snapshot.getMapTimeouts());
activeNodes.addAll(snapshot.getActiveNodes());
backendNodes.addAll(snapshot.getBackendNodes());
}
private void becomeLeader() {
leader = true;
Set backendNodesCopy = new HashSet<>(backendNodes);
cleanupStaleEntries(id -> !backendNodesCopy.contains(id));
}
boolean isLeader() {
return leader;
}
ChangeDetails applyMapPut(UUID changeId, ObjectNode change) {
String mapName = change.get(JsonUtil.CHANGE_NAME).asText();
String key = change.get(JsonUtil.CHANGE_KEY).asText();
JsonNode expectedValue = change.get(JsonUtil.CHANGE_EXPECTED_VALUE);
JsonNode expectedId = change.get(JsonUtil.CHANGE_EXPECTED_ID);
JsonNode newValue = change.get(JsonUtil.CHANGE_VALUE);
Map map = namedMapData.computeIfAbsent(mapName,
name -> new HashMap<>());
JsonNode oldValue = map.containsKey(key) ? map.get(key).data
: NullNode.getInstance();
UUID oldChangeId = map.containsKey(key) ? map.get(key).revisionId
: null;
if (expectedId != null
&& !Objects.equals(oldChangeId, JsonUtil.toUUID(expectedId))) {
return null;
}
if (expectedValue != null && !Objects.equals(oldValue, expectedValue)) {
return null;
}
if (newValue instanceof NullNode) {
map.remove(key);
} else {
map.put(key, new Entry(changeId, newValue.deepCopy(),
JsonUtil.toUUID(change.get(JsonUtil.CHANGE_SCOPE_OWNER))));
}
return new MapChange(mapName, MapChangeType.PUT, key, oldValue,
newValue, JsonUtil.toUUID(expectedId), changeId);
}
ChangeDetails applyMapReplace(UUID changeId, ObjectNode change) {
String mapName = change.get(JsonUtil.CHANGE_NAME).asText();
String key = change.get(JsonUtil.CHANGE_KEY).asText();
JsonNode expectedValue = change.get(JsonUtil.CHANGE_EXPECTED_VALUE);
JsonNode newValue = change.get(JsonUtil.CHANGE_VALUE);
Map map = namedMapData.computeIfAbsent(mapName,
name -> new HashMap<>());
JsonNode oldValue = map.containsKey(key) ? map.get(key).data
: NullNode.getInstance();
if (expectedValue != null && !Objects.equals(oldValue, expectedValue)) {
return null;
}
if (newValue instanceof NullNode) {
map.remove(key);
} else {
map.put(key, new Entry(changeId, newValue.deepCopy(),
JsonUtil.toUUID(change.get(JsonUtil.CHANGE_SCOPE_OWNER))));
}
return new MapChange(mapName, MapChangeType.REPLACE, key, oldValue,
newValue, null, changeId);
}
ChangeDetails applyListInsert(UUID id, ObjectNode change, boolean before) {
String listName = change.get(JsonUtil.CHANGE_NAME).asText();
UUID key = JsonUtil.toUUID(change.get(JsonUtil.CHANGE_POSITION_KEY));
JsonNode item = change.get(JsonUtil.CHANGE_VALUE);
UUID scopeOwnerId = JsonUtil
.toUUID(change.get(JsonUtil.CHANGE_SCOPE_OWNER));
if (Objects.equals(scopeOwnerId, JsonUtil.TOPIC_SCOPE_ID)) {
scopeOwnerId = null;
}
EntryList list = getOrCreateList(listName);
if (!conditionsMet(change)) {
return null;
}
ListEntrySnapshot insertedEntry;
if (key == null) {
if (before) { // insert before null -> insert last
insertedEntry = list.insertLast(id, item, id, scopeOwnerId);
} else { // insert after null -> insert first
insertedEntry = list.insertFirst(id, item, id, scopeOwnerId);
}
} else {
ListEntrySnapshot entry = list.getEntry(key);
if (entry == null) {
return null;
}
if (before) {
insertedEntry = list.insertBefore(key, id, item, id,
scopeOwnerId);
} else {
insertedEntry = list.insertAfter(key, id, item, id,
scopeOwnerId);
}
}
return new ListChange(listName, ListChangeType.INSERT, id, null, item,
null, insertedEntry.prev, null, insertedEntry.next, null, id);
}
ChangeDetails applyListMove(UUID id, ObjectNode change, boolean before) {
String listName = change.get(JsonUtil.CHANGE_NAME).asText();
UUID positionKey = JsonUtil
.toUUID(change.get(JsonUtil.CHANGE_POSITION_KEY));
UUID changeKey = JsonUtil.toUUID(change.get(JsonUtil.CHANGE_KEY));
UUID scopeOwnerId = JsonUtil
.toUUID(change.get(JsonUtil.CHANGE_SCOPE_OWNER));
EntryList list = getOrCreateList(listName);
if (!conditionsMet(change)) {
return null;
}
ListEntrySnapshot positionEntry = list.getEntry(positionKey);
if (positionEntry == null) {
return null;
}
ListEntrySnapshot moveEntry = list.getEntry(changeKey);
if (moveEntry == null) {
return null;
}
ListEntrySnapshot insertedEntry;
if (before) {
insertedEntry = list.moveBefore(positionKey, changeKey, id,
scopeOwnerId);
} else {
insertedEntry = list.moveAfter(positionKey, changeKey, id,
scopeOwnerId);
}
return new ListChange(listName, ListChangeType.MOVE, changeKey,
moveEntry.value, moveEntry.value, moveEntry.prev,
insertedEntry.prev, moveEntry.next, insertedEntry.next, null,
id);
}
private ChangeDetails applyListSet(UUID trackingId, ObjectNode change) {
String listName = change.get(JsonUtil.CHANGE_NAME).asText();
UUID key = JsonUtil.toUUID(change.get(JsonUtil.CHANGE_KEY));
JsonNode newValue = change.get(JsonUtil.CHANGE_VALUE);
UUID expectedId = JsonUtil
.toUUID(change.get(JsonUtil.CHANGE_EXPECTED_ID));
EntryList list = getOrCreateList(listName);
if (!conditionsMet(change)) {
return null;
}
ListEntrySnapshot entry = list.getEntry(key);
if (entry == null) {
return null;
}
if (expectedId != null
&& !Objects.equals(entry.revisionId, expectedId)) {
return null;
}
if (newValue.isNull()) {
list.remove(key);
return new ListChange(listName, ListChangeType.SET, key,
entry.value, null, entry.prev, null, entry.next, null,
expectedId, null);
} else {
JsonNode oldValue = entry.value;
UUID scopeOwnerId = JsonUtil
.toUUID(change.get(JsonUtil.CHANGE_SCOPE_OWNER));
if (Objects.equals(scopeOwnerId, JsonUtil.TOPIC_SCOPE_ID)) {
scopeOwnerId = null;
}
list.setValue(key, newValue, trackingId, scopeOwnerId);
return new ListChange(listName, ListChangeType.SET, key, oldValue,
newValue, entry.prev, entry.prev, entry.next, entry.next,
expectedId, trackingId);
}
}
private boolean conditionsMet(ObjectNode change) {
String listName = change.get(JsonUtil.CHANGE_NAME).asText();
EntryList list = getOrCreateList(listName);
if (change.has(JsonUtil.CHANGE_EMPTY)) {
boolean empty = change.get(JsonUtil.CHANGE_EMPTY).asBoolean();
if (empty && list.size() > 0) {
return false;
} else if (!empty && list.size() == 0) {
return false;
}
}
for (JsonNode condition : change
.withArray(JsonUtil.CHANGE_CONDITIONS)) {
UUID leftKey = JsonUtil.toUUID(condition.get(JsonUtil.CHANGE_KEY));
UUID rightKey = JsonUtil
.toUUID(condition.get(JsonUtil.CHANGE_POSITION_KEY));
// If the left key of the condition is null, right key must be the
// first i.e. have a null prev otherwise we reject the operation
if (leftKey == null
&& getListEntry(listName, rightKey).prev != null) {
return false;
} else if (leftKey != null && !Objects
.equals(getListEntry(listName, leftKey).next, rightKey)) {
return false;
}
}
for (JsonNode valueCondition : change
.withArray(JsonUtil.CHANGE_VALUE_CONDITIONS)) {
UUID refKey = JsonUtil
.toUUID(valueCondition.get(JsonUtil.CHANGE_KEY));
JsonNode expectedValue = valueCondition
.get(JsonUtil.CHANGE_EXPECTED_VALUE);
if (refKey == null || getListEntry(listName, refKey) == null
|| !Objects.equals(getListEntry(listName, refKey).value,
expectedValue)) {
return false;
}
}
return true;
}
void applyMapTimeout(ObjectNode change) {
String mapName = change.get(JsonUtil.CHANGE_NAME).asText();
JsonNode newValue = change.get(JsonUtil.CHANGE_VALUE);
if (newValue instanceof NullNode) {
mapExpirationTimeouts.remove(mapName);
} else {
Duration timeout = JsonUtil.toInstance(newValue, Duration.class);
mapExpirationTimeouts.put(mapName, timeout);
}
}
void applyListTimeout(ObjectNode change) {
String listName = change.get(JsonUtil.CHANGE_NAME).asText();
JsonNode newValue = change.get(JsonUtil.CHANGE_VALUE);
if (newValue instanceof NullNode) {
listExpirationTimeouts.remove(listName);
} else {
Duration timeout = JsonUtil.toInstance(newValue, Duration.class);
listExpirationTimeouts.put(listName, timeout);
}
}
Stream getListChanges(String listName) {
return getListItems(listName).map(item -> new ListChange(listName,
ListChangeType.INSERT, item.id, null, item.value, null,
item.prev, null, null, null, item.revisionId));
}
Stream getListItems(String listName) {
return getList(listName).map(EntryList::stream)
.orElseGet(Stream::empty);
}
ListEntrySnapshot getListEntry(String listName, UUID key) {
return getList(listName).map(list -> list.getEntry(key)).orElse(null);
}
JsonNode getListValue(String listName, UUID key) {
return getList(listName).map(list -> list.getValue(key)).orElse(null);
}
private EntryList getOrCreateList(String listName) {
return namedListData.computeIfAbsent(listName, name -> new EntryList());
}
private Optional getList(String listName) {
return Optional.ofNullable(namedListData.get(listName));
}
void setChangeResultTracker(UUID id,
SerializableConsumer changeResultTracker) {
SerializableConsumer oldTracker = changeResultTrackers
.putIfAbsent(id, changeResultTracker);
if (oldTracker != null) {
throw new IllegalStateException(
"Cannot set a change-result tracker for an id with one already set");
}
}
// For testing
boolean hasChangeListeners() {
return !changeListeners.isEmpty();
}
}