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.convergencelabs.convergence.server.api.realtime.ModelClientActor.scala Maven / Gradle / Ivy
/*
* Copyright (c) 2019 - Convergence Labs, Inc.
*
* This file is part of the Convergence Server, which is released under
* the terms of the GNU General Public License version 3 (GPLv3). A copy
* of the GPLv3 should have been provided along with this file, typically
* located in the "LICENSE" file, which is part of this source code package.
* Alternatively, see for the
* full text of the GPLv3 license, if it was not provided.
*/
package com.convergencelabs.convergence.server.api.realtime
import akka.actor.typed._
import akka.actor.typed.scaladsl.AskPattern._
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors, TimerScheduler}
import akka.util.Timeout
import com.convergencelabs.convergence.proto.core._
import com.convergencelabs.convergence.proto.model.ModelOfflineSubscriptionChangeRequestMessage.ModelOfflineSubscriptionData
import com.convergencelabs.convergence.proto.model.ModelsQueryResponseMessage.ModelResult
import com.convergencelabs.convergence.proto.model.OfflineModelUpdatedMessage.{ModelUpdateData, OfflineModelInitialData, OfflineModelUpdateData}
import com.convergencelabs.convergence.proto.model.{ModelPermissionsData => ProtoModelPermissions, ObjectValue => _, _}
import com.convergencelabs.convergence.proto.{ClientMessage, ModelMessage, NormalMessage, RequestMessage}
import com.convergencelabs.convergence.server.api.realtime.ClientActor.{SendServerMessage, SendServerRequest}
import com.convergencelabs.convergence.server.api.realtime.ProtocolConnection.ReplyCallback
import com.convergencelabs.convergence.server.api.realtime.protocol.CommonProtoConverters._
import com.convergencelabs.convergence.server.api.realtime.protocol.DataValueProtoConverters._
import com.convergencelabs.convergence.server.api.realtime.protocol.IdentityProtoConverters._
import com.convergencelabs.convergence.server.api.realtime.protocol.ModelPermissionConverters._
import com.convergencelabs.convergence.server.api.realtime.protocol.{JsonProtoConverters, OperationConverters}
import com.convergencelabs.convergence.server.backend.services.domain.model.ot.Operation
import com.convergencelabs.convergence.server.backend.services.domain.model.reference.RangeReference
import com.convergencelabs.convergence.server.backend.services.domain.model.{ModelServiceActor, RealtimeModelActor}
import com.convergencelabs.convergence.server.model.DomainId
import com.convergencelabs.convergence.server.model.domain.model._
import com.convergencelabs.convergence.server.model.domain.session.DomainSessionAndUserId
import com.convergencelabs.convergence.server.model.domain.user.DomainUserId
import com.convergencelabs.convergence.server.util.serialization.akka.CborSerializable
import com.fasterxml.jackson.annotation.{JsonSubTypes, JsonTypeInfo}
import com.google.protobuf.struct.Value
import com.google.protobuf.struct.Value.Kind.{NumberValue => ProtoNumber, StringValue => ProtoString}
import grizzled.slf4j.Logging
import org.json4s.JsonAST.{JInt, JString, JValue}
import scalapb.GeneratedMessage
import java.time.Instant
import java.util.UUID
import java.util.concurrent.TimeoutException
import scala.collection.mutable.ListBuffer
import scala.concurrent.ExecutionContextExecutor
import scala.concurrent.duration.FiniteDuration
import scala.language.postfixOps
/**
* The [[ModelClientActor]] handles all incoming and outgoing messages
* that are specific to the Model subsystem.
*
* @param context The ActorContext for this actor.
* @param timers The Akka timers instance to use for
* scheduling.
* @param domainId The domain this client has connected to.
* @param session The unique session for this client.
* @param clientActor The client actor managing the web socket
* communication.
* @param modelStoreActor The model store actor store for this domain.
* @param modelClusterRegion The model shard region for this domain.
* @param requestTimeout The default request timeout.
* @param offlineModelSyncInterval Specifies how often should offline model
* synchronization should occur.
*/
private final class ModelClientActor(context: ActorContext[ModelClientActor.Message],
timers: TimerScheduler[ModelClientActor.Message],
domainId: DomainId,
session: DomainSessionAndUserId,
clientActor: ActorRef[ClientActor.SendToClient],
modelStoreActor: ActorRef[ModelServiceActor.Message],
modelClusterRegion: ActorRef[RealtimeModelActor.Message],
requestTimeout: Timeout,
offlineModelSyncInterval: FiniteDuration)
extends AbstractBehavior[ModelClientActor.Message](context) with Logging {
import ModelClientActor._
private[this] implicit val ec: ExecutionContextExecutor = context.executionContext
private[this] implicit val system: ActorSystem[_] = context.system
private[this] implicit val defaultRequestTimeout: Timeout = requestTimeout
private[this] val resourceManager = new ResourceManager[String]()
private[this] var subscribedModels = Map[String, OfflineModelState]()
private[this] var openingModelStash = Map[String, ListBuffer[OutgoingMessage]]()
timers.startTimerAtFixedRate(SyncTaskTimer, SyncOfflineModels, offlineModelSyncInterval)
override def onMessage(msg: ModelClientActor.Message): Behavior[ModelClientActor.Message] = {
msg match {
case IncomingProtocolMessage(message) =>
onMessageReceived(message)
case IncomingProtocolRequest(message, replyPromise) =>
onRequestReceived(message.asInstanceOf[RequestMessage with ModelMessage], replyPromise)
case message: OutgoingMessage =>
onOutgoingModelMessage(message)
case SyncOfflineModels =>
syncOfflineModels(this.subscribedModels)
case message: UpdateOfflineModel =>
handleOfflineModelSynced(message)
case message: ModelClosed =>
onModelClosed(message)
case message: ModelOpenSuccess =>
onModelOpenSuccess(message)
case message: ModelOpenFailure =>
onModelOpenFailure(message)
case message: ModelResyncRequestSuccess =>
onModelResyncSuccess(message)
case message: ModelResyncRequestFailure =>
onModelResyncFailure(message)
}
Behaviors.same
}
override def onSignal: PartialFunction[Signal, Behavior[ModelClientActor.Message]] = super.onSignal orElse {
case PostStop =>
timers.cancelAll()
Behaviors.same
}
private[this] def handleOfflineModelSynced(message: UpdateOfflineModel): Unit = {
val UpdateOfflineModel(modelId, action) = message
action match {
case ModelServiceActor.OfflineModelInitial(model, permissions, valueIdPrefix) =>
this.subscribedModels.get(modelId).foreach { _ =>
val modelDataUpdate = ModelUpdateData(
model.metaData.version,
Some(instantToTimestamp(model.metaData.createdTime)),
Some(instantToTimestamp(model.metaData.modifiedTime)),
Some(objectValueToProto(model.data))
)
val permissionsData = ProtoModelPermissions(permissions.read, permissions.write, permissions.remove, permissions.manage)
val prefix = java.lang.Long.toString(valueIdPrefix, 36)
val initialData = OfflineModelInitialData(model.metaData.collection, prefix, Some(modelDataUpdate), Some(permissionsData))
val action = OfflineModelUpdatedMessage.Action.Initial(initialData)
val message = OfflineModelUpdatedMessage(modelId, action)
clientActor ! SendServerMessage(message)
val version = model.metaData.version
this.subscribedModels += modelId -> OfflineModelState(version, permissions)
}
case ModelServiceActor.OfflineModelUpdated(model, permissions) =>
this.subscribedModels.get(modelId).foreach { currentState =>
val modelUpdate = model.map { m =>
ModelUpdateData(
m.metaData.version,
Some(instantToTimestamp(m.metaData.createdTime)),
Some(instantToTimestamp(m.metaData.modifiedTime)),
Some(objectValueToProto(m.data))
)
}
val permissionsUpdate = permissions.map { p =>
ProtoModelPermissions(p.read, p.write, p.remove, p.manage)
}
val updateData = OfflineModelUpdateData(modelUpdate, permissionsUpdate)
val action = OfflineModelUpdatedMessage.Action.Updated(updateData)
val message = OfflineModelUpdatedMessage(modelId, action)
clientActor ! SendServerMessage(message)
val version = model.map(_.metaData.version).getOrElse(currentState.currentVersion)
val perms = permissions.getOrElse(currentState.currentPermissions)
this.subscribedModels += modelId -> OfflineModelState(version, perms)
}
case ModelServiceActor.OfflineModelDeleted() =>
val message = OfflineModelUpdatedMessage(modelId, OfflineModelUpdatedMessage.Action.Deleted(true))
clientActor ! SendServerMessage(message)
this.subscribedModels -= modelId
case ModelServiceActor.OfflineModelPermissionRevoked() =>
val message = OfflineModelUpdatedMessage(modelId, OfflineModelUpdatedMessage.Action.PermissionRevoked(true))
clientActor ! SendServerMessage(message)
this.subscribedModels -= modelId
case ModelServiceActor.OfflineModelNotUpdate() =>
// No update required
}
}
private[this] def syncOfflineModels(models: Map[String, OfflineModelState]): Unit = {
// FIXME handle the error conditions here better.
val notOpen = models.filter { case (modelId, _) => !this.resourceManager.hasId(modelId) }
notOpen.foreach { case (modelId, OfflineModelState(version, permissions)) =>
modelStoreActor.ask[ModelServiceActor.GetModelUpdateResponse](
ModelServiceActor.GetModelUpdateRequest(domainId, modelId, version, permissions, this.session.userId, _))
.map(_.result.fold(
{
case ModelServiceActor.UnknownError() =>
error("Error updating offline model")
},
{
action => context.self ! UpdateOfflineModel(modelId, action)
}))
.recover(cause => error("Error updating offline model", cause))
}
}
//
// Outgoing Messages
//
private[this] def onOutgoingModelMessage(message: OutgoingMessage): Unit = {
if (openingModelStash.contains(message.modelId)) {
message match {
case autoCreateRequest: ClientAutoCreateModelConfigRequest =>
// This message is part of the opening process and should go
// out immediately. All others should be stashed.
onAutoCreateModelConfigRequest(autoCreateRequest)
case _ =>
val stashedMessages = openingModelStash(message.modelId)
stashedMessages += message
}
} else {
message match {
case op: OutgoingOperation =>
onOutgoingOperation(op)
case opAck: OperationAcknowledgement =>
onOperationAcknowledgement(opAck)
case remoteOpened: RemoteClientOpened =>
onRemoteClientOpened(remoteOpened)
case remoteClosed: RemoteClientClosed =>
onRemoteClientClosed(remoteClosed)
case forceClosed: ModelForceClose =>
onModelForceClose(forceClosed)
case _: ClientAutoCreateModelConfigRequest =>
logger.warn(s"$domainId: Received a ClientAutoCreateModelConfigRequest for a model that is not opening: ${message.modelId}")
case refShared: RemoteReferenceShared =>
onRemoteReferenceShared(refShared)
case refUnshared: RemoteReferenceUnshared =>
onRemoteReferenceUnshared(refUnshared)
case refSet: RemoteReferenceSet =>
onRemoteReferenceSet(refSet)
case refCleared: RemoteReferenceCleared =>
onRemoteReferenceCleared(refCleared)
case permsChanged: ModelPermissionsChanged =>
onModelPermissionsChanged(permsChanged)
case message: ModelResyncServerComplete =>
onModelResyncServerComplete(message)
case resyncStarted: RemoteClientResyncStarted =>
onRemoteClientResyncStarted(resyncStarted)
case resyncCompleted: RemoteClientResyncCompleted =>
onRemoteClientResyncCompleted(resyncCompleted)
case ServerError(_, ExpectedError(code, message, details)) =>
val errorMessage = ErrorMessage(code, message, JsonProtoConverters.jValueMapToValueMap(details))
clientActor ! SendServerMessage(errorMessage)
}
}
}
private[this] def onOutgoingOperation(op: OutgoingOperation): Unit = {
val OutgoingOperation(modelId, session, contextVersion, timestamp, operation) = op
resourceId(modelId) foreach { resourceId =>
val message = RemoteOperationMessage(
resourceId,
session.sessionId,
contextVersion,
Some(instantToTimestamp(timestamp)),
Some(OperationConverters.mapOutgoing(operation)))
clientActor ! SendServerMessage(message)
this.subscribedModels.get(modelId).foreach(state => {
val newState = state.copy(currentVersion = contextVersion)
this.subscribedModels += (modelId -> newState)
})
}
}
private[this] def onOperationAcknowledgement(opAck: OperationAcknowledgement): Unit = {
val OperationAcknowledgement(modelId, seqNo, version, timestamp) = opAck
resourceId(modelId) foreach { resourceId =>
val message = OperationAcknowledgementMessage(resourceId, seqNo, version, Some(instantToTimestamp(timestamp)))
clientActor ! SendServerMessage(message)
}
}
private[this] def onRemoteClientOpened(opened: RemoteClientOpened): Unit = {
val RemoteClientOpened(modelId, session) = opened
resourceId(modelId) foreach { resourceId =>
val serverMessage = RemoteClientOpenedMessage(resourceId, session.sessionId)
clientActor ! SendServerMessage(serverMessage)
}
}
private[this] def onRemoteClientClosed(closed: RemoteClientClosed): Unit = {
val RemoteClientClosed(modelId, session) = closed
resourceId(modelId) foreach { resourceId =>
val serverMessage = RemoteClientClosedMessage(resourceId, session.sessionId)
clientActor ! SendServerMessage(serverMessage)
}
}
private[this] def onModelPermissionsChanged(permsChanged: ModelPermissionsChanged): Unit = {
val ModelPermissionsChanged(modelId, permissions) = permsChanged
resourceId(modelId) foreach { resourceId =>
val serverMessage = ModelPermissionsChangedMessage(resourceId, Some(modelPermissionsToProto(permissions)))
clientActor ! SendServerMessage(serverMessage)
this.subscribedModels.get(modelId).foreach(state => {
val newState = state.copy(currentPermissions = permissions)
this.subscribedModels += (modelId -> newState)
})
}
}
private[this] def onModelForceClose(forceClose: ModelForceClose): Unit = {
val ModelForceClose(modelId, reason, reasonCode) = forceClose
resourceId(modelId) foreach { resourceId =>
logger.warn(s"$domainId: Model forced closed: {modelId: '$modelId', resourceId: $resourceId")
this.resourceManager.releaseResource(resourceId)
openingModelStash -= modelId
val serverMessage = ModelForceCloseMessage(resourceId, reason, reasonCode.id)
clientActor ! SendServerMessage(serverMessage)
}
}
private[this] def onAutoCreateModelConfigRequest(autoConfigRequest: ClientAutoCreateModelConfigRequest): Unit = {
val ClientAutoCreateModelConfigRequest(_, autoConfigId, replyTo) = autoConfigRequest
clientActor.ask[Any](SendServerRequest(AutoCreateModelConfigRequestMessage(autoConfigId), _))
.mapTo[AutoCreateModelConfigResponseMessage]
.map {
case AutoCreateModelConfigResponseMessage(collection, protoData, overridePermissions, worldPermissionsData, userPermissionsData, ephemeral, _) =>
val data = protoData match {
case Some(value) =>
protoToObjectValue(value).map(Some(_))
case None =>
Right(None)
}
data fold( { _ =>
ClientAutoCreateModelConfigResponse(Left(ClientAutoCreateModelConfigInvalid()))
}, { data =>
val worldPermissions = worldPermissionsData.map {
case ProtoModelPermissions(read, write, remove, manage, _) =>
ModelPermissions(read, write, remove, manage)
}
val userPermissions = modelUserPermissionSeqToMap(userPermissionsData)
val config = ClientAutoCreateModelConfig(
collection,
data,
Some(overridePermissions),
worldPermissions,
userPermissions,
Some(ephemeral))
ClientAutoCreateModelConfigResponse(Right(config))
})
}
.recover {
case _: TimeoutException =>
ClientAutoCreateModelConfigResponse(Left(ClientAutoCreateModelConfigTimeout()))
case cause: ClassCastException =>
warn("Client returned invalid data for model auto create config", cause)
ClientAutoCreateModelConfigResponse(Left(ClientAutoCreateModelConfigInvalid()))
case cause: Throwable =>
error("Unexpected error getting auto create config from client", cause)
ClientAutoCreateModelConfigResponse(Left(UnknownError()))
}
.foreach(replyTo ! _)
}
private[this] def onModelResyncServerComplete(message: ModelResyncServerComplete): Unit = {
val ModelResyncServerComplete(modelId, connectedClients, resyncingClients, references) = message
resourceId(modelId) foreach { resourceId =>
val convertedReferences = convertReferences(references)
val serverMessage = ModelResyncServerCompleteMessage(
resourceId,
connectedClients.map(s => s.sessionId).toSeq,
resyncingClients.map(s => s.sessionId).toSeq,
convertedReferences)
clientActor ! SendServerMessage(serverMessage)
}
}
private[this] def onRemoteReferenceShared(refShared: RemoteReferenceShared): Unit = {
val RemoteReferenceShared(modelId, session, valueId, key, values) = refShared
resourceId(modelId) foreach { resourceId =>
val references = mapOutgoingReferenceValue(values)
val serverMessage = RemoteReferenceSharedMessage(resourceId, valueId, key, Some(references), session.sessionId)
clientActor ! SendServerMessage(serverMessage)
}
}
private[this] def onRemoteReferenceUnshared(refUnshared: RemoteReferenceUnshared): Unit = {
val RemoteReferenceUnshared(modelId, session, valueId, key) = refUnshared
resourceId(modelId) foreach { resourceId =>
val serverMessage = RemoteReferenceUnsharedMessage(resourceId, valueId, key, session.sessionId)
clientActor ! SendServerMessage(serverMessage)
}
}
private[this] def onRemoteReferenceSet(refSet: RemoteReferenceSet): Unit = {
val RemoteReferenceSet(modelId, session, valueId, key, values) = refSet
resourceId(modelId) foreach { resourceId =>
val references = mapOutgoingReferenceValue(values)
val serverMessage = RemoteReferenceSetMessage(resourceId, valueId, key, Some(references), session.sessionId)
clientActor ! SendServerMessage(serverMessage)
}
}
private[this] def onRemoteClientResyncStarted(message: RemoteClientResyncStarted): Unit = {
val RemoteClientResyncStarted(modelId, remoteSession) = message
resourceId(modelId) foreach { resourceId =>
val serverMessage = RemoteClientResyncStartedMessage(resourceId, remoteSession.sessionId)
clientActor ! SendServerMessage(serverMessage)
}
}
private[this] def onRemoteClientResyncCompleted(message: RemoteClientResyncCompleted): Unit = {
val RemoteClientResyncCompleted(modelId, remoteSession) = message
resourceId(modelId) foreach { resourceId =>
val serverMessage = RemoteClientResyncCompletedMessage(resourceId, remoteSession.sessionId)
clientActor ! SendServerMessage(serverMessage)
}
}
private[this] def mapOutgoingReferenceValue(values: ModelReferenceValues): ReferenceValues = {
values match {
case IndexReferenceValues(indices) =>
ReferenceValues().withIndices(Int32List(indices))
case RangeReferenceValues(ranges) =>
val protoRanges = ranges.map {
case RangeReference.Range(from, to) => IndexRange(from, to)
}
ReferenceValues().withRanges(IndexRangeList(protoRanges))
case PropertyReferenceValues(properties) =>
ReferenceValues().withProperties(StringList(properties))
case ElementReferenceValues(elementIds) =>
ReferenceValues().withElements(StringList(elementIds))
}
}
private[this] def mapIncomingReference(values: ReferenceValues): Either[Unit, ModelReferenceValues] = {
values.values match {
case ReferenceValues.Values.Indices(Int32List(indices, _)) =>
Right(IndexReferenceValues(indices.toList))
case ReferenceValues.Values.Ranges(IndexRangeList(ranges, _)) =>
val mapped = ranges.map(r => RangeReference.Range(r.startIndex, r.endIndex)).toList
Right(RangeReferenceValues(mapped))
case ReferenceValues.Values.Properties(StringList(properties, _)) =>
Right(PropertyReferenceValues(properties.toList))
case ReferenceValues.Values.Elements(StringList(elements, _)) =>
Right(ElementReferenceValues(elements.toList))
case ReferenceValues.Values.Empty =>
Left(())
}
}
private[this] def onRemoteReferenceCleared(refCleared: RemoteReferenceCleared): Unit = {
val RemoteReferenceCleared(modelId, session, valueId, key) = refCleared
resourceId(modelId) foreach { resourceId =>
val serverMessage = RemoteReferenceClearedMessage(resourceId, valueId, key, session.sessionId)
clientActor ! SendServerMessage(serverMessage)
}
}
//
// Incoming Messages
//
private[this] def onRequestReceived(message: RequestMessage, replyCallback: ReplyCallback): Unit = {
message match {
case openRequest: OpenRealtimeModelRequestMessage =>
onOpenRealtimeModelRequest(openRequest, replyCallback)
case resyncRequest: ModelResyncRequestMessage =>
onModelResyncRequest(resyncRequest, replyCallback)
case closeRequest: CloseRealtimeModelRequestMessage =>
onCloseRealtimeModelRequest(closeRequest, replyCallback)
case createRequest: CreateRealtimeModelRequestMessage =>
onCreateRealtimeModelRequest(createRequest, replyCallback)
case deleteRequest: DeleteRealtimeModelRequestMessage =>
onDeleteRealtimeModelRequest(deleteRequest, replyCallback)
case queryRequest: ModelsQueryRequestMessage =>
onModelQueryRequest(queryRequest, replyCallback)
case getPermissionRequest: GetModelPermissionsRequestMessage =>
onGetModelPermissionsRequest(getPermissionRequest, replyCallback)
case setPermissionRequest: SetModelPermissionsRequestMessage =>
onSetModelPermissionsRequest(setPermissionRequest, replyCallback)
case message: ModelOfflineSubscriptionChangeRequestMessage =>
onModelOfflineSubscription(message, replyCallback)
}
}
private[this] def onMessageReceived(message: NormalMessage with ModelMessage): Unit = {
message match {
case message: OperationSubmissionMessage =>
onOperationSubmission(message)
case message: ShareReferenceMessage =>
onShareReference(message)
case message: UnshareReferenceMessage =>
onUnshareReference(message)
case message: SetReferenceMessage =>
onSetReference(message)
case message: ClearReferenceMessage =>
onClearReference(message)
case message: ModelResyncClientCompleteMessage =>
onModelResyncClientComplete(message)
}
}
private[this] def onOperationSubmission(message: OperationSubmissionMessage): Unit = {
val OperationSubmissionMessage(resourceId, seqNo, version, operation, _) = message
this.resourceManager.getId(resourceId) match {
case Some(modelId) =>
operation match {
case Some(op) =>
OperationConverters.mapIncoming(op).fold({ _ =>
warn(s"$domainId: Received an operation submissions with an invalid operation: $message")
invalidOperation(message)
},
{ mappedOp =>
val submission = RealtimeModelActor.OperationSubmission(domainId, modelId, session, seqNo, version, mappedOp)
modelClusterRegion ! submission
})
case None =>
warn(s"$domainId: Received an operation submissions with an empty operation: $message")
invalidOperation(message)
}
case None =>
warn(s"$domainId: Received an operation submissions for a resourceId that does not exist: $resourceId")
val serverMessage = unknownResourceId(resourceId)
clientActor ! SendServerMessage(serverMessage)
}
}
private[this] def onModelResyncClientComplete(message: ModelResyncClientCompleteMessage): Unit = {
val ModelResyncClientCompleteMessage(resourceId, open, _) = message
this.resourceManager.getId(resourceId) match {
case Some(modelId) =>
val message = RealtimeModelActor.ModelResyncClientComplete(domainId, modelId, session, open)
modelClusterRegion ! message
case None =>
warn(s"$domainId: Received model resync client complete message for an unknown resource id.")
val serverMessage = unknownResourceId(resourceId)
clientActor ! SendServerMessage(serverMessage)
}
}
/////////////////////////////////////////////////////////////////////////////
// References
/////////////////////////////////////////////////////////////////////////////
private[this] def onShareReference(message: ShareReferenceMessage): Unit = {
val ShareReferenceMessage(resourceId, valueId, key, references, version, _) = message
val vId = valueId.filter(_.nonEmpty)
this.resourceManager.getId(resourceId) match {
case Some(modelId) =>
references match {
case Some(refs) =>
mapIncomingReference(refs).map { values =>
val publishReference = RealtimeModelActor.ShareReference(domainId, modelId, session, vId, key, values, version)
modelClusterRegion ! publishReference
}.left.map { _ =>
invalidReferenceType(message.toString)
}
case None =>
noReferenceValues(message.toString)
}
case None =>
noResourceIdForReferenceMessage(resourceId, message.toString)
}
}
def onUnshareReference(message: UnshareReferenceMessage): Unit = {
val UnshareReferenceMessage(resourceId, valueId, key, _) = message
val vId = valueId.filter(_.nonEmpty)
this.resourceManager.getId(resourceId) match {
case Some(modelId) =>
val unshareReference = RealtimeModelActor.UnShareReference(domainId, modelId, session, vId, key)
modelClusterRegion ! unshareReference
case None =>
noResourceIdForReferenceMessage(resourceId, message.toString)
}
}
private[this] def onSetReference(message: SetReferenceMessage): Unit = {
val SetReferenceMessage(resourceId, valueId, key, references, version, _) = message
val vId = valueId.filter(_.nonEmpty)
this.resourceManager.getId(resourceId) match {
case Some(modelId) =>
references match {
case Some(refs) =>
mapIncomingReference(refs).map { values =>
val setReference = RealtimeModelActor.SetReference(domainId, modelId, session, vId, key, values, version)
modelClusterRegion ! setReference
}.left.map { _ =>
invalidReferenceType(message.toString)
}
case None =>
noReferenceValues(message.toString)
}
case None =>
noResourceIdForReferenceMessage(resourceId, message.toString)
}
}
private[this] def onClearReference(message: ClearReferenceMessage): Unit = {
val ClearReferenceMessage(resourceId, valueId, key, _) = message
val vId = valueId.filter(_.nonEmpty)
this.resourceManager.getId(resourceId) match {
case Some(modelId) =>
val clearReference = RealtimeModelActor.ClearReference(domainId, modelId, session, vId, key)
modelClusterRegion ! clearReference
case None =>
warn(s"$domainId: Received a reference clear message for a resource id that does not exists.")
val serverMessage = unknownResourceId(resourceId)
clientActor ! SendServerMessage(serverMessage)
}
}
private[this] def convertReferences(references: Set[ReferenceState]): Seq[ReferenceData] = {
references.map {
case ReferenceState(sessionId, valueId, key, values) =>
val referenceValues = mapOutgoingReferenceValue(values)
ReferenceData(sessionId.sessionId, valueId, key, Some(referenceValues))
}.toSeq
}
private[this] def noResourceIdForReferenceMessage(resourceId: Long, message: String): Unit = {
warn(s"$domainId: Received a reference message for a resource id that does not exists $message")
val serverMessage = unknownResourceId(resourceId)
clientActor ! SendServerMessage(serverMessage)
}
private[this] def noReferenceValues(message: String): Unit = {
warn(s"$domainId: Received a reference set with no reference values.")
val errorMessage = ErrorMessage(
ErrorCodes.InvalidMessage.toString,
s"Invalid reference set message. No reference values were specified: $message")
clientActor ! SendServerMessage(errorMessage)
}
private[this] def invalidReferenceType(message: String): Unit = {
warn(s"$domainId: Received a reference set with an invalid reference type.")
val errorMessage = ErrorMessage(
ErrorCodes.InvalidMessage.toString,
"Invalid reference set message. Invalid reference type: " + message)
clientActor ! SendServerMessage(errorMessage)
}
private[this] def onModelOfflineSubscription(message: ModelOfflineSubscriptionChangeRequestMessage, replyCallback: ReplyCallback): Unit = {
val ModelOfflineSubscriptionChangeRequestMessage(subscribe, unsubscribe, all, _) = message
val previousModels = this.subscribedModels.keySet
if (all) {
this.subscribedModels = Map()
}
unsubscribe.foreach(modelId => this.subscribedModels -= modelId)
subscribe.foreach { case ModelOfflineSubscriptionData(modelId, version, permissions, _) =>
val ProtoModelPermissions(read, write, remove, manage, _) = permissions.getOrElse(NoPermissions)
val state = OfflineModelState(version, ModelPermissions(read, write, remove, manage))
this.subscribedModels += modelId -> state
}
val newModels = this.subscribedModels.filter {
case (modelId, _) => !previousModels.contains(modelId)
}
this.syncOfflineModels(newModels)
replyCallback.reply(OkResponse())
}
private[this] def onCloseRealtimeModelRequest(request: CloseRealtimeModelRequestMessage, cb: ReplyCallback): Unit = {
val CloseRealtimeModelRequestMessage(resourceId, _) = request
this.resourceManager.getId(resourceId) match {
case Some(modelId) =>
modelClusterRegion
.ask[RealtimeModelActor.CloseRealtimeModelResponse](
RealtimeModelActor.CloseRealtimeModelRequest(domainId, modelId, session, _))
.map(_.response.fold(
{
case RealtimeModelActor.ModelNotOpenError() =>
cb.expectedError(ErrorCodes.ModelNotOpen, s"The model '$modelId' could not be closed because it was not open.'")
case RealtimeModelActor.UnknownError() =>
cb.unexpectedError("An unexpected error occurred while closing the model.")
},
{ _ =>
context.self ! ModelClosed(modelId, resourceId, cb)
})
)
.recover { cause =>
warn("A timeout occurred closing a model", cause)
cb.timeoutError()
}
case None =>
cb.expectedError(ErrorCodes.ModelNotOpen, s"the requested model was not open")
}
}
private[this] def onModelClosed(message: ModelClosed): Unit = {
val ModelClosed(_, resourceId, cb) = message
this.resourceManager.releaseResource(resourceId)
cb.reply(CloseRealTimeModelResponseMessage())
}
private[this] def onOpenRealtimeModelRequest(request: OpenRealtimeModelRequestMessage, cb: ReplyCallback): Unit = {
val OpenRealtimeModelRequestMessage(optionalModelId, autoCreateId, _) = request
val modelId = getSetOrRandomModelId(optionalModelId)
openingModelStash += (modelId -> ListBuffer())
val narrowedSelf = context.self.narrow[OutgoingMessage]
modelClusterRegion.ask[RealtimeModelActor.OpenRealtimeModelResponse](
RealtimeModelActor.OpenRealtimeModelRequest(domainId, modelId, autoCreateId, session, narrowedSelf, _))
.map(_.response.fold(
error => context.self ! ModelOpenFailure(modelId, Right(error), cb),
message => context.self ! ModelOpenSuccess(modelId, message, cb)
))
.recover(cause => context.self ! ModelOpenFailure(modelId, Left(cause), cb))
}
private[this] def onModelOpenFailure(message: ModelOpenFailure): Unit = {
val ModelOpenFailure(modelId, failure, cb) = message
// Remove the message stash since we didn't open we aren't going to
// send the messages on.
openingModelStash -= modelId
failure
.map {
case RealtimeModelActor.ModelAlreadyOpenError() =>
ModelClientActor.modelAlreadyOpenError(cb, modelId)
case RealtimeModelActor.ModelAlreadyOpeningError() =>
ModelClientActor.modelAlreadyOpeningError(cb, modelId)
case RealtimeModelActor.ModelClosingAfterErrorError() =>
modelClosingAfterErrorError(cb, modelId)
case RealtimeModelActor.ModelDeletedWhileOpeningError() =>
ModelClientActor.modelDeletedError(cb, modelId)
case RealtimeModelActor.ClientDataRequestError(message) =>
cb.expectedError(ErrorCodes.ModelClientDataRequestFailure, message)
case RealtimeModelActor.ModelNotFoundError() =>
ModelClientActor.modelNotFoundError(cb, modelId)
case RealtimeModelActor.UnauthorizedError(message) =>
cb.reply(ErrorMessages.Unauthorized(message))
case RealtimeModelActor.UnknownError() =>
cb.unknownError()
case RealtimeModelActor.ClientErrorResponse(message) =>
cb.expectedError(ErrorCodes.ModelClientDataRequestFailure, message)
}
.left
.map { cause =>
warn("A timeout occurred waiting for an open model request", cause)
cb.timeoutError()
}
}
private[this] def onModelOpenSuccess(message: ModelOpenSuccess): Unit = {
val ModelOpenSuccess(modelId, success, cb) = message
val RealtimeModelActor.OpenModelSuccess(valueIdPrefix, metaData, connectedClients, resyncingClients, references, modelData, modelPermissions) = success
val resourceId = this.resourceManager.getOrAssignResource(modelId)
val convertedReferences = convertReferences(references)
cb.reply(
OpenRealtimeModelResponseMessage(
resourceId,
metaData.id,
metaData.collection,
java.lang.Long.toString(valueIdPrefix, 36),
metaData.version,
Some(instantToTimestamp(metaData.createdTime)),
Some(instantToTimestamp(metaData.modifiedTime)),
Some(objectValueToProto(modelData)),
connectedClients.map(s => s.sessionId).toSeq,
resyncingClients.map(s => s.sessionId).toSeq,
convertedReferences,
Some(ProtoModelPermissions(
modelPermissions.read,
modelPermissions.write,
modelPermissions.remove,
modelPermissions.manage))))
flushOpenStash(modelId)
}
private[this] def flushOpenStash(modelId: String): Unit = {
val stashedMessages = openingModelStash.getOrElse(modelId, ListBuffer())
// Note: We must remove the stash before processing the messages or
// else they would be re stashed.
openingModelStash -= modelId
// replay these messages into the outgoing message handler to get
// them shipped off to the client.
stashedMessages.foreach(message => onOutgoingModelMessage(message))
}
private[this] def onModelResyncRequest(request: ModelResyncRequestMessage, cb: ReplyCallback): Unit = {
val ModelResyncRequestMessage(modelId, contextVersion, _) = request
val narrowedSelf = context.self.narrow[OutgoingMessage]
openingModelStash += (modelId -> ListBuffer())
modelClusterRegion.ask[RealtimeModelActor.ModelResyncResponse](
RealtimeModelActor.ModelResyncRequest(domainId, modelId, session, contextVersion, narrowedSelf, _))
.map(_.response.fold(
error => context.self ! ModelResyncRequestFailure(modelId, Right(error), cb),
data => context.self ! ModelResyncRequestSuccess(modelId, data, cb)
))
.recover(cause => context.self ! ModelResyncRequestFailure(modelId, Left(cause), cb))
}
private[this] def onModelResyncSuccess(message: ModelResyncRequestSuccess): Unit = {
val ModelResyncRequestSuccess(modelId, data, cb) = message
val RealtimeModelActor.ModelResyncResponseData(currentVersion, permissions) = data
val ModelPermissions(read, write, remove, manage) = permissions
val permissionData = ProtoModelPermissions(read, write, remove, manage)
val resourceId = this.resourceManager.getOrAssignResource(modelId)
val responseMessage = ModelResyncResponseMessage(resourceId, currentVersion, Some(permissionData))
cb.reply(responseMessage)
flushOpenStash(modelId)
}
private[this] def onModelResyncFailure(message: ModelResyncRequestFailure): Unit = {
val ModelResyncRequestFailure(modelId, failure, cb) = message
openingModelStash -= modelId
failure
.map {
case RealtimeModelActor.ModelAlreadyOpenError() =>
ModelClientActor.modelAlreadyOpenError(cb, modelId)
case RealtimeModelActor.ModelAlreadyOpeningError() =>
ModelClientActor.modelAlreadyOpeningError(cb, modelId)
case RealtimeModelActor.ModelClosingAfterErrorError() =>
modelClosingAfterErrorError(cb, modelId)
case RealtimeModelActor.ModelNotFoundError() =>
ModelClientActor.modelNotFoundError(cb, modelId)
case RealtimeModelActor.UnauthorizedError(message) =>
cb.reply(ErrorMessages.Unauthorized(message))
case RealtimeModelActor.UnknownError() =>
cb.unknownError()
}
.left
.map { cause =>
warn("A timeout occurred waiting for an model resync request", cause)
cb.timeoutError()
}
}
private[this] def onCreateRealtimeModelRequest(request: CreateRealtimeModelRequestMessage, cb: ReplyCallback): Unit = {
val CreateRealtimeModelRequestMessage(
collectionId,
optionalModelId,
data,
createdTime,
overridePermissions,
worldPermissionsData,
userPermissionsData,
_) = request
data match {
case Some(createData) =>
protoToObjectValue(createData).fold(
{ _ =>
cb.expectedError(ErrorCodes.InvalidMessage, "Invalid model data was provided in create model request")
},
{ modelRoot =>
val worldPermissions = worldPermissionsData.map(w =>
ModelPermissions(w.read, w.write, w.remove, w.manage))
val userPermissions = modelUserPermissionSeqToMap(userPermissionsData)
val modelId = getSetOrRandomModelId(optionalModelId)
modelClusterRegion
.ask[RealtimeModelActor.CreateRealtimeModelResponse](
RealtimeModelActor.CreateRealtimeModelRequest(
domainId,
modelId,
collectionId,
modelRoot,
createdTime.map(timestampToInstant),
Some(overridePermissions),
worldPermissions,
userPermissions,
Some(session),
_))
.map(_.response.fold(
{
case RealtimeModelActor.ModelAlreadyExistsError(fingerprint) =>
val data: Map[String, JValue] = fingerprint
.map(fp => Map("fingerprint" -> JString(fp)))
.getOrElse(Map())
cb.expectedError(
ErrorCodes.ModelAlreadyExists,
s"A model with the id '$modelId' already exists.",
data)
case RealtimeModelActor.UnauthorizedError(message) =>
cb.reply(ErrorMessages.Unauthorized(message))
case RealtimeModelActor.InvalidCreationDataError(message) =>
cb.expectedError(
ErrorCodes.InvalidModelCreationData,
message,
Map())
case RealtimeModelActor.CollectionDoesNotExistError(message) =>
cb.expectedError(
ErrorCodes.CollectionDoesNotExist,
message,
Map("collectionId" -> JString(collectionId)))
case RealtimeModelActor.UnknownError() =>
cb.unexpectedError("could not create model")
},
modelId => cb.reply(CreateRealtimeModelResponseMessage(modelId))
))
.recover { cause =>
warn("A timeout occurred waiting for an close model request", cause)
cb.timeoutError()
}
}
)
case None =>
cb.expectedError(ErrorCodes.InvalidMessage, "No model data was provided in create model request")
}
}
private[this] def onDeleteRealtimeModelRequest(request: DeleteRealtimeModelRequestMessage, cb: ReplyCallback): Unit = {
val DeleteRealtimeModelRequestMessage(modelId, _) = request
// We may or may not be able to delete the model, but the user has obviously unsubscribed.
this.subscribedModels -= request.modelId
modelClusterRegion
.ask[RealtimeModelActor.DeleteRealtimeModelResponse](
RealtimeModelActor.DeleteRealtimeModelRequest(domainId, modelId, Some(session), _))
.map(_.response.fold(
{
case RealtimeModelActor.ModelNotFoundError() =>
ModelClientActor.modelNotFoundError(cb, modelId)
case RealtimeModelActor.UnauthorizedError(message) =>
cb.reply(ErrorMessages.Unauthorized(message))
case RealtimeModelActor.UnknownError() =>
cb.unexpectedError(s"An unexpected error occurred while attempting to delete model '$modelId'")
},
{ _ =>
cb.reply(DeleteRealtimeModelResponseMessage())
}
))
.recover { cause =>
warn("A timeout occurred waiting for a delete model request", cause)
cb.timeoutError()
}
}
private[this] def onModelQueryRequest(request: ModelsQueryRequestMessage, cb: ReplyCallback): Unit = {
val ModelsQueryRequestMessage(query, _) = request
modelStoreActor.ask[ModelServiceActor.QueryModelsResponse](
ModelServiceActor.QueryModelsRequest(domainId, session.userId, query, _))
.map(_.result.fold(
{
case ModelServiceActor.InvalidQueryError(message, _, index) =>
val details = index.map(i => Map("index" -> JInt(i))).getOrElse(Map())
cb.expectedError(ErrorCodes.ModelInvalidQuery, message, details)
case ModelServiceActor.UnknownError() =>
cb.unexpectedError("Unexpected error querying models.")
},
{ result =>
val models = result.data.map {
r =>
ModelResult(
r.metaData.collection,
r.metaData.id,
Some(instantToTimestamp(r.metaData.createdTime)),
Some(instantToTimestamp(r.metaData.modifiedTime)),
r.metaData.version,
Some(JsonProtoConverters.toStruct(r.data)))
}
cb.reply(ModelsQueryResponseMessage(models, result.offset, result.count))
}
))
.recover { cause =>
warn("A timeout occurred waiting for a model query request", cause)
cb.timeoutError()
}
}
private[this] def onGetModelPermissionsRequest(request: GetModelPermissionsRequestMessage, cb: ReplyCallback): Unit = {
val GetModelPermissionsRequestMessage(modelId, _) = request
modelClusterRegion
.ask[RealtimeModelActor.GetModelPermissionsResponse](
RealtimeModelActor.GetModelPermissionsRequest(domainId, modelId, session, _))
.map(_.response.fold(
{
case RealtimeModelActor.ModelNotFoundError() =>
ModelClientActor.modelNotFoundError(cb, modelId)
case RealtimeModelActor.UnauthorizedError(message) =>
cb.reply(ErrorMessages.Unauthorized(message))
case RealtimeModelActor.UnknownError() =>
cb.unexpectedError("could get model permissions")
},
{
case RealtimeModelActor.GetModelPermissionsSuccess(overridesCollection, world, users) =>
val mappedWorld = ProtoModelPermissions(world.read, world.write, world.remove, world.manage)
val mappedUsers = modelUserPermissionSeqToMap(users)
cb.reply(GetModelPermissionsResponseMessage(overridesCollection, Some(mappedWorld), mappedUsers))
}
))
.recover { cause =>
warn("A timeout occurred getting model permissions", cause)
cb.timeoutError()
}
}
private[this] def onSetModelPermissionsRequest(request: SetModelPermissionsRequestMessage, cb: ReplyCallback): Unit = {
val SetModelPermissionsRequestMessage(modelId, overridePermissions, world, setAllUsers, addedUsers, removedUsers, _) = request
val mappedWorld = world map (w => ModelPermissions(w.read, w.write, w.remove, w.manage))
val mappedAddedUsers = modelUserPermissionSeqToMap(addedUsers)
modelClusterRegion
.ask[RealtimeModelActor.SetModelPermissionsResponse](
RealtimeModelActor.SetModelPermissionsRequest(
domainId,
modelId,
session,
overridePermissions,
mappedWorld,
setAllUsers,
mappedAddedUsers,
removedUsers.map(protoToDomainUserId).toList,
_))
.map(_.response.fold(
{
case RealtimeModelActor.ModelNotFoundError() =>
ModelClientActor.modelNotFoundError(cb, modelId)
case RealtimeModelActor.UnauthorizedError(message) =>
cb.reply(ErrorMessages.Unauthorized(message))
case RealtimeModelActor.UnknownError() =>
cb.unexpectedError("could set model permissions")
},
{ _ =>
cb.reply(SetModelPermissionsResponseMessage())
}
))
.recover { cause =>
warn("A timeout occurred setting model permissions", cause)
cb.timeoutError()
}
}
private[this] def resourceId(modelId: String): Option[Int] = {
this.resourceManager.getResource(modelId) orElse {
error(s"$domainId: Receive an outgoing message for a modelId that is not open: $modelId")
None
}
}
private[this] def getSetOrRandomModelId(optionalModelId: Option[String]): String = {
optionalModelId.filter(_.nonEmpty).getOrElse(UUID.randomUUID().toString)
}
}
object ModelClientActor {
private[realtime] def apply(domain: DomainId,
session: DomainSessionAndUserId,
clientActor: ActorRef[ClientActor.SendToClient],
modelStoreActor: ActorRef[ModelServiceActor.Message],
modelShardRegion: ActorRef[RealtimeModelActor.Message],
requestTimeout: Timeout,
offlineModelSyncInterval: FiniteDuration): Behavior[Message] =
Behaviors.setup { context =>
Behaviors.withTimers { timers =>
new ModelClientActor(context, timers, domain, session, clientActor, modelStoreActor, modelShardRegion, requestTimeout, offlineModelSyncInterval)
}
}
private val NoPermissions = ProtoModelPermissions(read = false, write = false, remove = false, manage = false)
private final case object SyncTaskTimer
private def modelNotFoundError(cb: ReplyCallback, id: String): Unit = {
val details = Map("id" -> JString(id))
val message = s"A model with id '$id' does not exist."
cb.expectedError(ErrorCodes.ModelNotFound, message, details)
}
private def invalidOperation(operation: OperationSubmissionMessage): ErrorMessage = {
val opString = operation.operation.map(_.toString).getOrElse("no operation supplied")
ErrorMessage(
ErrorCodes.ModelInvalidOperation.toString,
s"An operation submission contained an invalid operation.",
Map("operation" -> Value(ProtoString(opString))))
}
private def unknownResourceId(resourceId: Long) = ErrorMessage(
ErrorCodes.ModelUnknownResourceId.toString,
s"A model with resource id '$resourceId' does not exist.",
Map("resourceId" -> Value(ProtoNumber(resourceId.toDouble))))
private def modelAlreadyOpenError(cb: ReplyCallback, id: String): Unit = {
val details = Map("id" -> JString(id))
val message = s"The model with id '$id' is already open."
cb.expectedError(ErrorCodes.ModelAlreadyOpen, message, details)
}
private def modelAlreadyOpeningError(cb: ReplyCallback, id: String): Unit = {
val details = Map("id" -> JString(id))
val message = s"The model with id '$id' is already being opened by this client."
cb.expectedError(ErrorCodes.ModelAlreadyOpening, message, details)
}
private def modelClosingAfterErrorError(cb: ReplyCallback, id: String): Unit = {
val details = Map("id" -> JString(id))
val message = s"The model is currently shutting down after an error. You may be able to reopen the model."
cb.expectedError(ErrorCodes.ModelClosingAfterError, message, details)
}
private def modelDeletedError(cb: ReplyCallback, id: String): Unit = {
val details = Map("id" -> JString(id))
val message = s"The model with id '$id' was deleted."
cb.expectedError(ErrorCodes.ModelDeleted, message, details)
}
private final case object SyncOfflineModels extends Message
private final case class OfflineModelState(currentVersion: Long, currentPermissions: ModelPermissions)
private final case class UpdateOfflineModel(modelId: String, action: ModelServiceActor.ModelUpdateResult) extends Message
/////////////////////////////////////////////////////////////////////////////
// Message Protocol
/////////////////////////////////////////////////////////////////////////////
sealed trait Message extends CborSerializable
//
// Messages from the client
//
private[realtime] sealed trait IncomingMessage extends Message
private[realtime] type IncomingNormalMessage = GeneratedMessage with NormalMessage with ModelMessage with ClientMessage
private[realtime] final case class IncomingProtocolMessage(message: IncomingNormalMessage) extends IncomingMessage
private[realtime] type IncomingRequestMessage = GeneratedMessage with RequestMessage with ModelMessage with ClientMessage
private[realtime] final case class IncomingProtocolRequest(message: IncomingRequestMessage, replyCallback: ReplyCallback) extends IncomingMessage
//
// Messages from the server
//
trait OutgoingMessage extends Message {
val modelId: String
}
final case class ServerError(modelId: String, expectedError: ExpectedError) extends OutgoingMessage
final case class OperationAcknowledgement(modelId: String, seqNo: Int, contextVersion: Long, timestamp: Instant) extends OutgoingMessage
final case class OutgoingOperation(modelId: String,
session: DomainSessionAndUserId,
contextVersion: Long,
timestamp: Instant,
operation: Operation) extends OutgoingMessage
final case class RemoteClientClosed(modelId: String, session: DomainSessionAndUserId) extends OutgoingMessage
final case class RemoteClientOpened(modelId: String, session: DomainSessionAndUserId) extends OutgoingMessage
final case class RemoteClientResyncStarted(modelId: String, session: DomainSessionAndUserId) extends OutgoingMessage
final case class RemoteClientResyncCompleted(modelId: String, session: DomainSessionAndUserId) extends OutgoingMessage
final case class ModelResyncServerComplete(modelId: String,
connectedClients: Set[DomainSessionAndUserId],
resyncingClients: Set[DomainSessionAndUserId],
references: Set[ReferenceState]) extends OutgoingMessage
object ForceModelCloseReasonCode extends Enumeration {
val Unknown, Unauthorized, Deleted, ServerShutdown, ErrorApplyingOperation,
InvalidReferenceEvent, PermissionError, UnexpectedCommittedVersion, PermissionsChanged = Value
}
final case class ModelForceClose(modelId: String, reason: String, reasonCode: ForceModelCloseReasonCode.Value) extends OutgoingMessage
final case class ModelPermissionsChanged(modelId: String, permissions: ModelPermissions) extends OutgoingMessage
final case class ClientAutoCreateModelConfigRequest(modelId: String, autoConfigId: Int, replyTo: ActorRef[ClientAutoCreateModelConfigResponse]) extends OutgoingMessage
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes(Array(
new JsonSubTypes.Type(value = classOf[ClientAutoCreateModelConfigTimeout], name = "timeout"),
new JsonSubTypes.Type(value = classOf[ClientAutoCreateModelConfigInvalid], name = "invalid"),
new JsonSubTypes.Type(value = classOf[UnknownError], name = "unknown")
))
sealed trait ClientAutoCreateModelConfigError
final case class ClientAutoCreateModelConfigTimeout() extends ClientAutoCreateModelConfigError
final case class ClientAutoCreateModelConfigInvalid() extends ClientAutoCreateModelConfigError
final case class UnknownError() extends ClientAutoCreateModelConfigError
final case class ClientAutoCreateModelConfigResponse(config: Either[ClientAutoCreateModelConfigError, ClientAutoCreateModelConfig]) extends CborSerializable
final case class ClientAutoCreateModelConfig(collectionId: String,
modelData: Option[ObjectValue],
overridePermissions: Option[Boolean],
worldPermissions: Option[ModelPermissions],
userPermissions: Map[DomainUserId, ModelPermissions],
ephemeral: Option[Boolean])
sealed trait RemoteReferenceEvent extends OutgoingMessage
final case class RemoteReferenceShared(modelId: String, session: DomainSessionAndUserId,
id: Option[String],
key: String,
values: ModelReferenceValues) extends RemoteReferenceEvent
final case class RemoteReferenceSet(modelId: String,
session: DomainSessionAndUserId,
id: Option[String],
key: String,
values: ModelReferenceValues) extends RemoteReferenceEvent
final case class RemoteReferenceCleared(modelId: String, session: DomainSessionAndUserId, id: Option[String], key: String) extends RemoteReferenceEvent
final case class RemoteReferenceUnshared(modelId: String, session: DomainSessionAndUserId, id: Option[String], key: String) extends RemoteReferenceEvent
private final case class ModelClosed(modelId: String, resourceId: Int, cb: ReplyCallback) extends Message
private final case class ModelOpenSuccess(modelId: String, message: RealtimeModelActor.OpenModelSuccess, cb: ReplyCallback) extends Message
private final case class ModelOpenFailure(modelId: String, failure: Either[Throwable, RealtimeModelActor.OpenRealtimeModelError], cb: ReplyCallback) extends Message
private final case class ModelResyncRequestSuccess(modelId: String, data: RealtimeModelActor.ModelResyncResponseData, cb: ReplyCallback) extends Message
private final case class ModelResyncRequestFailure(modelId: String, failure: Either[Throwable, RealtimeModelActor.ModelResyncError], cb: ReplyCallback) extends Message
}