games.mythical.saga.sdk.client.observer.SagaStatusUpdateObserver Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of saga-sdk-java Show documentation
Show all versions of saga-sdk-java Show documentation
Saga SDK for Java game servers
package games.mythical.saga.sdk.client.observer;
import com.google.protobuf.Struct;
import com.google.protobuf.Value;
import games.mythical.saga.sdk.client.executor.*;
import games.mythical.saga.sdk.client.model.SagaItem;
import games.mythical.saga.sdk.client.model.SagaItemUpdate;
import games.mythical.saga.sdk.client.model.SagaUserAmount;
import games.mythical.saga.sdk.exception.ErrorData;
import games.mythical.saga.sdk.exception.SagaErrorCode;
import games.mythical.saga.sdk.exception.SagaException;
import games.mythical.saga.sdk.exception.SubError;
import games.mythical.saga.sdk.proto.api.itemtype.FailedItemTypeBatch;
import games.mythical.saga.sdk.proto.streams.StatusUpdate;
import games.mythical.saga.sdk.proto.streams.currency.CurrencyUpdate;
import games.mythical.saga.sdk.proto.streams.currencytype.CurrencyTypeUpdate;
import games.mythical.saga.sdk.proto.streams.item.ItemUpdate;
import games.mythical.saga.sdk.proto.streams.itemtype.ItemTypeUpdate;
import games.mythical.saga.sdk.proto.streams.metadata.MetadataUpdate;
import games.mythical.saga.sdk.proto.streams.playerwallet.PlayerWalletUpdate;
import games.mythical.saga.sdk.proto.streams.reservation.ReservationUpdate;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@Slf4j
public final class SagaStatusUpdateObserver extends AbstractObserver {
private static final String PING_TRACE = "ping";
private static SagaStatusUpdateObserver instance;
private final Consumer resubscribe;
private SagaCurrencyExecutor sagaCurrencyExecutor;
private SagaCurrencyTypeExecutor sagaCurrencyTypeExecutor;
private SagaItemExecutor sagaItemExecutor;
private SagaItemTypeExecutor sagaItemTypeExecutor;
private SagaPlayerWalletExecutor sagaPlayerWalletExecutor;
private SagaReservationExecutor sagaReservationExecutor;
private SagaMetadataExecutor sagaMetadataExecutor;
public SagaStatusUpdateObserver(Consumer resubscribe) {
this.resubscribe = resubscribe;
}
public static SagaStatusUpdateObserver getInstance() {
return instance;
}
public static SagaStatusUpdateObserver initialize(Consumer resubscribe) {
instance = new SagaStatusUpdateObserver(resubscribe);
return instance;
}
public static void clear() {
instance = null;
}
public SagaStatusUpdateObserver with(SagaCurrencyExecutor sagaCurrencyExecutor) {
this.sagaCurrencyExecutor = sagaCurrencyExecutor;
return this;
}
public SagaStatusUpdateObserver with(SagaCurrencyTypeExecutor sagaCurrencyTypeExecutor) {
this.sagaCurrencyTypeExecutor = sagaCurrencyTypeExecutor;
return this;
}
public SagaStatusUpdateObserver with(SagaItemExecutor sagaItemExecutor) {
this.sagaItemExecutor = sagaItemExecutor;
return this;
}
public SagaStatusUpdateObserver with(SagaItemTypeExecutor sagaItemTypeExecutor) {
this.sagaItemTypeExecutor = sagaItemTypeExecutor;
return this;
}
public SagaStatusUpdateObserver with(SagaPlayerWalletExecutor sagaPlayerWalletExecutor) {
this.sagaPlayerWalletExecutor = sagaPlayerWalletExecutor;
return this;
}
public SagaStatusUpdateObserver with(SagaReservationExecutor sagaReservationExecutor) {
this.sagaReservationExecutor = sagaReservationExecutor;
return this;
}
public SagaStatusUpdateObserver with(SagaMetadataExecutor sagaMetadataExecutor) {
this.sagaMetadataExecutor = sagaMetadataExecutor;
return this;
}
@Override
public void onNext(StatusUpdate message) {
log.trace("StatusUpdateObserver.onNext for event {} with message {}", message.getStatusUpdateCase(), message.getTraceId());
resetConnectionRetry();
if (message.getTraceId().equalsIgnoreCase(PING_TRACE)) {
return;
}
try {
switch (message.getStatusUpdateCase()) {
case CURRENCY_UPDATE:
handleCurrencyUpdate(message.getCurrencyUpdate(), message.getTraceId());
break;
case CURRENCY_TYPE_UPDATE:
handleCurrencyTypeUpdate(message.getCurrencyTypeUpdate(), message.getTraceId());
break;
case ITEM_UPDATE:
handleItemUpdate(message.getItemUpdate(), message.getTraceId());
break;
case METADATA_UPDATE:
handleMetadataUpdate(message.getMetadataUpdate(), message.getTraceId());
break;
case ITEM_TYPE_UPDATE:
handleItemTypeUpdate(message.getItemTypeUpdate(), message.getTraceId());
break;
case PLAYER_WALLET_UPDATE:
handlePlayerWalletUpdate(message.getPlayerWalletUpdate(), message.getTraceId());
break;
case RESERVATION_UPDATE:
handleReservationUpdate(message.getReservationUpdate(), message.getTraceId());
break;
default: {
log.error("Unrecognized event {}", message.getStatusUpdateCase());
throw new SagaException(SagaErrorCode.UNRECOGNIZED);
}
}
} catch (Exception e) {
log.error("Exception calling executor action for message:{}. {}", message.getTraceId(), e);
}
}
@Override
public void onError(Throwable t) {
log.error("StatusUpdateObserver.onError", t);
sleepBetweenReconnects();
resubscribe.accept(this);
}
@Override
public void onCompleted() {
log.info("StatusUpdateObserver stream closed");
sleepBetweenReconnects();
resubscribe.accept(this);
}
private void handleCurrencyUpdate(CurrencyUpdate update, String traceId) throws Exception {
if (sagaCurrencyExecutor == null) {
log.debug("Currency update received, but no currency executor registered {}", update);
}
else {
if (update.hasError()) {
final var error = update.getError();
sagaCurrencyExecutor.onError(toErrData(error));
} else {
final var message = update.getStatusUpdate();
if (message.getBalancesList().isEmpty()) {
log.error("No balances were issued, this shouldn't have occurred. {}", traceId);
return;
}
switch (message.getCurrencyState()) {
case ISSUED:
var balances = message.getBalancesList().stream()
.map(balance -> new SagaUserAmount(balance.getOauthId(), balance.getBalanceInWei()))
.collect(Collectors.toList());
sagaCurrencyExecutor.currencyIssued(
message.getBalancesList().get(0).getCurrencyTypeId(),
message.getTransactionId(),
balances,
message.getIdempotencyId(),
traceId
);
break;
default:
var balance = message.getBalancesList().stream().findFirst().get();
sagaCurrencyExecutor.updateCurrency(
balance.getCurrencyTypeId(),
message.getTransactionId(),
balance.getOauthId(),
balance.getBalanceInWei(),
message.getIdempotencyId(),
traceId,
message.getCurrencyState()
);
break;
}
}
}
}
private void handleCurrencyTypeUpdate(CurrencyTypeUpdate update, String traceId) throws Exception {
if (sagaCurrencyTypeExecutor == null) {
log.debug("CurrencyType update received, but no currencytype executor registered {}", update);
}
else {
if (update.hasError()) {
final var error = update.getError();
sagaCurrencyTypeExecutor.onError(toErrData(error));
} else {
final var message = update.getStatusUpdate();
sagaCurrencyTypeExecutor.updateCurrencyType(
message.getCurrencyTypeId(),
message.getTransactionId(),
message.getContractAddress(),
message.getIdempotencyId(),
traceId,
message.getCurrencyTypeState()
);
}
}
}
private void handleItemUpdate(ItemUpdate update, String traceId) throws Exception {
if (sagaItemExecutor == null) {
log.debug("Item update received, but no item executor registered {}", update);
}
else {
if (update.hasError()) {
final var error = update.getError();
sagaItemExecutor.onError(toErrData(error));
} else if (update.hasStatusUpdate()) {
final var message = update.getStatusUpdate();
sagaItemExecutor.updateItem(
message.getInventoryId(),
message.getItemTypeId(),
message.getOauthId(),
message.getTokenId(),
message.getMetadataUrl(),
traceId,
message.getItemState()
);
} else {
final var updates = update.getStatusUpdates().getStatusUpdatesList().stream()
.map(SagaItemUpdate::fromProto).collect(Collectors.toList());
sagaItemExecutor.updateItems(updates, traceId);
}
}
}
private void handleMetadataUpdate(MetadataUpdate update, String traceId) throws Exception {
if (sagaMetadataExecutor == null) {
log.debug("Metadata update received, but no metadata executor registered {}", update);
}
else {
if (update.hasError()) {
final var error = update.getError();
sagaItemExecutor.onError(toErrData(error));
} else {
final var message = update.getMetadataUpdated();
sagaMetadataExecutor.updateMetadata(
message.getInventoryId(),
message.getMetadataUrl(),
traceId
);
}
}
}
private void handleItemTypeUpdate(ItemTypeUpdate update, String traceId) throws Exception {
if (sagaItemTypeExecutor == null) {
log.debug("Item type update received, but no item type executor registered {}", update);
}
else {
if (update.hasError()) {
final var error = update.getError();
sagaItemTypeExecutor.onError(toErrData(error));
} else {
final var message = update.getStatusUpdate();
sagaItemTypeExecutor.updateItemType(
message.getItemTypeId(),
traceId,
message.getItemTypeState()
);
}
}
}
private void handlePlayerWalletUpdate(PlayerWalletUpdate update, String traceId) throws Exception {
if (sagaPlayerWalletExecutor == null) {
log.debug("Player wallet update received, but no player wallet executor registered {}", update);
}
else {
if (update.hasError()) {
final var error = update.getError();
sagaPlayerWalletExecutor.onError(toErrData(error));
} else {
final var statusUpdate = update.getStatusUpdate();
sagaPlayerWalletExecutor.updatePlayerWallet(traceId, statusUpdate.getOauthId());
}
}
}
private void handleReservationUpdate(ReservationUpdate update, String traceId) {
if (sagaReservationExecutor == null) {
log.debug("Reservation update received, but no reservation executor registered {}", update);
} else {
switch (update.getUpdateCase()) {
case ERROR:
sagaReservationExecutor.onError(toErrData(update.getError()));
break;
case RESERVATION_CREATED:
sagaReservationExecutor.onReservationCreated(update.getReservationCreated().getReservationId(), traceId);
break;
case RESERVATION_RELEASED:
sagaReservationExecutor.onReservationReleased(update.getReservationReleased().getReservationId(), traceId);
break;
case RESERVATION_REDEEMED:
final var items = update.getReservationRedeemed().getItemsList().stream()
.map(SagaItem::fromProto).collect(Collectors.toList());
final var failedBatches = update.getReservationRedeemed().getFailedBatchesList().stream()
.map(FailedItemTypeBatch::getItemTypeId).collect(Collectors.toList());
sagaReservationExecutor.onReservationRedeemed(update.getReservationRedeemed().getReservationId(), items, failedBatches, traceId);
break;
default:
log.error("Unknown reservation update: {}", update.getUpdateCase());
break;
}
}
}
private Map toMetadata(Struct protoMetadata) {
final var metadataMap = new HashMap();
protoMetadata.getFieldsMap()
.forEach((key, value) -> metadataMap.put(key, metadataValueToString(value)));
return metadataMap;
}
private String metadataValueToString(Value value) {
if (value.hasNullValue()) {
return null;
} else if (value.hasStringValue()) {
return value.getStringValue();
}
return value.toString();
}
private SubError toSubError(games.mythical.saga.sdk.proto.common.SubError error) {
return SubError.builder()
.code(error.getErrorCode())
.message(error.getMessage())
.source(error.getSource())
.metadata(toMetadata(error.getMetadata()))
.build();
}
private ErrorData toErrData(games.mythical.saga.sdk.proto.common.ErrorData error) {
final var subErrors = error.getSuberrorsList().stream()
.map(this::toSubError)
.collect(Collectors.toList());
return ErrorData.builder()
.code(error.getErrorCode())
.trace(error.getTrace())
.source(error.getSource())
.message(error.getMessage())
.metadata(toMetadata(error.getMetadata()))
.suberrors(subErrors)
.build();
}
}