Skip to content

Commit ac4a7a4

Browse files
authored
feat(data): Update Events API Contract (#3029)
1 parent 6d820c5 commit ac4a7a4

File tree

9 files changed

+151
-67
lines changed

9 files changed

+151
-67
lines changed

appsync/aws-appsync-events/src/main/java/com/amplifyframework/aws/appsync/events/Events.kt

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import kotlinx.coroutines.coroutineScope
2424
import kotlinx.serialization.json.Json
2525
import kotlinx.serialization.json.JsonElement
2626
import okhttp3.OkHttpClient
27-
import org.jetbrains.annotations.VisibleForTesting
2827

2928
/**
3029
* The main class for interacting with AWS AppSync Events
@@ -33,16 +32,16 @@ import org.jetbrains.annotations.VisibleForTesting
3332
* @param connectAuthorizer for AWS AppSync Websocket Pub/Sub connection.
3433
* @param defaultChannelAuthorizers passed to created channels if not overridden.
3534
*/
36-
class Events @VisibleForTesting internal constructor(
35+
class Events(
3736
val endpoint: String,
3837
val connectAuthorizer: AppSyncAuthorizer,
3938
val defaultChannelAuthorizers: ChannelAuthorizers,
40-
options: Options,
41-
okHttpClient: OkHttpClient
39+
options: Options = Options()
4240
) {
4341

4442
data class Options(
45-
val loggerProvider: LoggerProvider? = null
43+
val loggerProvider: LoggerProvider? = null,
44+
val okHttpConfigurationProvider: OkHttpConfigurationProvider? = null
4645
)
4746

4847
/**
@@ -52,24 +51,15 @@ class Events @VisibleForTesting internal constructor(
5251
* @param connectAuthorizer for AWS AppSync Websocket Pub/Sub connection.
5352
* @param defaultChannelAuthorizers passed to created channels if not overridden.
5453
*/
55-
constructor(
56-
endpoint: String,
57-
connectAuthorizer: AppSyncAuthorizer,
58-
defaultChannelAuthorizers: ChannelAuthorizers,
59-
options: Options = Options()
60-
) : this(
61-
endpoint,
62-
connectAuthorizer,
63-
defaultChannelAuthorizers,
64-
options,
65-
OkHttpClient.Builder().build()
66-
)
6754

6855
private val json = Json {
6956
encodeDefaults = true
7057
ignoreUnknownKeys = true
7158
}
7259
private val endpoints = EventsEndpoints(endpoint)
60+
private val okHttpClient = OkHttpClient.Builder().apply {
61+
options.okHttpConfigurationProvider?.applyConfiguration(this)
62+
}.build()
7363
private val httpClient = RestClient(endpoints.restEndpoint, okHttpClient, json)
7464
private val eventsWebSocketProvider = EventsWebSocketProvider(
7565
endpoints,

appsync/aws-appsync-events/src/main/java/com/amplifyframework/aws/appsync/events/EventsChannel.kt

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -84,16 +84,11 @@ class EventsChannel internal constructor(
8484
* @param authorizer for the publish call. If not provided, the EventChannel publish authorizer will be used.
8585
* @return result of publish.
8686
*/
87-
@Throws(EventsException::class)
8887
suspend fun publish(
8988
event: JsonElement,
9089
authorizer: AppSyncAuthorizer = this.authorizers.publishAuthorizer
9190
): PublishResult {
92-
return try {
93-
publishToWebSocket(listOf(event), authorizer)
94-
} catch (exception: Exception) {
95-
throw exception.toEventsException()
96-
}
91+
return publish(listOf(event), authorizer)
9792
}
9893

9994
/**
@@ -103,22 +98,27 @@ class EventsChannel internal constructor(
10398
* @param authorizer for the publish call. If not provided, the EventChannel publish authorizer will be used.
10499
* @return result of publish.
105100
*/
106-
@Throws(Exception::class)
107101
suspend fun publish(
108102
events: List<JsonElement>,
109103
authorizer: AppSyncAuthorizer = this.authorizers.publishAuthorizer
110104
): PublishResult {
111105
return try {
112-
publishToWebSocket(events, authorizer)
106+
publishToWebSocket(events, authorizer).let {
107+
PublishResult.Response(
108+
successfulEvents = it.successfulEvents,
109+
failedEvents = it.failedEvents
110+
)
111+
}
113112
} catch (exception: Exception) {
114-
throw exception.toEventsException()
113+
PublishResult.Failure(exception.toEventsException())
115114
}
116115
}
117116

117+
@Throws(Exception::class)
118118
private suspend fun publishToWebSocket(
119119
events: List<JsonElement>,
120120
authorizer: AppSyncAuthorizer
121-
): PublishResult = coroutineScope {
121+
): WebSocketMessage.Received.PublishSuccess = coroutineScope {
122122
val publishId = UUID.randomUUID().toString()
123123
val publishMessage = WebSocketMessage.Send.Publish(
124124
id = publishId,
@@ -136,19 +136,16 @@ class EventsChannel internal constructor(
136136

137137
return@coroutineScope when (val response = deferredResponse.await()) {
138138
is WebSocketMessage.Received.PublishSuccess -> {
139-
PublishResult(response.successfulEvents, response.failedEvents)
139+
response
140140
}
141-
142141
is WebSocketMessage.ErrorContainer -> {
143142
val fallbackMessage = "Failed to publish event(s)"
144143
throw response.errors.firstOrNull()?.toEventsException(fallbackMessage)
145144
?: EventsException(fallbackMessage)
146145
}
147-
148146
is WebSocketMessage.Closed -> {
149147
throw response.reason.toCloseException()
150148
}
151-
152149
else -> throw EventsException("Received unexpected publish response of type: ${response::class}")
153150
}
154151
}

appsync/aws-appsync-events/src/main/java/com/amplifyframework/aws/appsync/events/EventsWebSocket.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,8 @@ internal class EventsWebSocket(
212212
url(eventsEndpoints.websocketRealtimeEndpoint)
213213
addHeader(HeaderKeys.SEC_WEBSOCKET_PROTOCOL, HeaderValues.SEC_WEBSOCKET_PROTOCOL_APPSYNC_EVENTS)
214214
addHeader(HeaderKeys.HOST, eventsEndpoints.restEndpoint.host)
215+
addHeader(HeaderKeys.USER_AGENT, HeaderValues.USER_AGENT)
216+
addHeader(HeaderKeys.X_AMZ_USER_AGENT, HeaderValues.USER_AGENT)
215217
}.build()
216218
}
217219

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
package com.amplifyframework.aws.appsync.events
16+
17+
import okhttp3.OkHttpClient
18+
19+
/**
20+
* An OkHttpConfigurationProvider is a hook provided to a customer, enabling them to customize
21+
* the OkHttp client used by the Events Library.
22+
*
23+
* This hook is for advanced use cases, such as where a user may want to append some of
24+
* their own request headers, configure timeouts, or otherwise manipulate an outgoing request.
25+
*/
26+
fun interface OkHttpConfigurationProvider {
27+
/**
28+
* The OkHttp.Builder() used for the Events library is provided. This mutable builder allows for setting custom
29+
* configurations on the OkHttp.Builder() instance. The library will run this configuration when the Events
30+
* class is constructed and then build() the OkHttp client which will be used for all library network calls.
31+
* @param okHttpClientBuilder An [OkHttpClient.Builder] instance
32+
*/
33+
fun applyConfiguration(okHttpClientBuilder: OkHttpClient.Builder)
34+
}

appsync/aws-appsync-events/src/main/java/com/amplifyframework/aws/appsync/events/RestClient.kt

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,19 @@ internal class RestClient(
4545
}
4646

4747
suspend fun post(channelName: String, authorizer: AppSyncAuthorizer, events: List<JsonElement>): PublishResult {
48+
return try {
49+
executePost(channelName, authorizer, events)
50+
} catch (exception: Exception) {
51+
PublishResult.Failure(exception.toEventsException())
52+
}
53+
}
54+
55+
@Throws(Exception::class)
56+
internal suspend fun executePost(
57+
channelName: String,
58+
authorizer: AppSyncAuthorizer,
59+
events: List<JsonElement>
60+
): PublishResult.Response {
4861
val postBody = JsonObject(
4962
content = mapOf(
5063
"channel" to JsonPrimitive(channelName),
@@ -57,6 +70,8 @@ internal class RestClient(
5770
addHeader(HeaderKeys.ACCEPT, HeaderValues.ACCEPT_APPLICATION_JSON)
5871
addHeader(HeaderKeys.CONTENT_TYPE, HeaderValues.CONTENT_TYPE_APPLICATION_JSON)
5972
addHeader(HeaderKeys.HOST, url.host)
73+
addHeader(HeaderKeys.USER_AGENT, HeaderValues.USER_AGENT)
74+
addHeader(HeaderKeys.X_AMZ_USER_AGENT, HeaderValues.USER_AGENT)
6075
post(postBody.toRequestBody(HeaderValues.CONTENT_TYPE_APPLICATION_JSON.toMediaType()))
6176
}.build()
6277

@@ -80,7 +95,7 @@ internal class RestClient(
8095
val result = okHttpClient.newCall(authRequest).execute()
8196
val body = result.body.string()
8297
return if (result.isSuccessful) {
83-
json.decodeFromString<PublishResult>(body)
98+
json.decodeFromString<PublishResult.Response>(body)
8499
} else {
85100
throw try {
86101
val errors = json.decodeFromString<EventsErrors>(body)

appsync/aws-appsync-events/src/main/java/com/amplifyframework/aws/appsync/events/data/EventsException.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
*/
1515
package com.amplifyframework.aws.appsync.events.data
1616

17+
import java.net.SocketTimeoutException
1718
import java.net.UnknownHostException
1819

1920
/**
@@ -43,10 +44,10 @@ open class EventsException internal constructor(
4344
}
4445
}
4546

46-
fun Exception.toEventsException(): EventsException {
47+
internal fun Exception.toEventsException(): EventsException {
4748
return when (this) {
4849
is EventsException -> this
49-
is UnknownHostException -> NetworkException(throwable = this)
50+
is UnknownHostException, is SocketTimeoutException -> NetworkException(throwable = this)
5051
else -> EventsException.unknown(cause = this)
5152
}
5253
}

appsync/aws-appsync-events/src/main/java/com/amplifyframework/aws/appsync/events/data/PublishResult.kt

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,38 +18,51 @@ import kotlinx.serialization.SerialName
1818
import kotlinx.serialization.Serializable
1919

2020
/**
21-
* Contains the result of an event(s) publish call.
21+
* Sealed Result type of an event(s) publish call.
22+
* PublishResult.Success = The publish call was successfully processed by the service
23+
* PublishResult.Failure = The publish call did not succeed
2224
*
23-
* @property successfulEvents list of events successfully processed by AWS AppSync.
24-
* @property failedEvents list of events that AWS AppSync failed to process.
25-
* @property status of the publish call.
26-
* Successful = All events published successfully
27-
* Failed = All events failed to publish
28-
* PartialSuccess = Mix of successful and failed events. Check event indexes to determine individual states.
2925
*/
30-
@Serializable
31-
data class PublishResult internal constructor(
32-
@SerialName("successful") val successfulEvents: List<SuccessfulEvent>,
33-
@SerialName("failed") val failedEvents: List<FailedEvent>
34-
) {
26+
sealed class PublishResult {
3527

3628
/**
37-
* Contains identifying information of an event AWS AppSync failed to process.
38-
*/
39-
sealed class Status {
40-
data object Successful : Status()
41-
data object Failed : Status()
42-
data object PartialSuccess : Status()
43-
}
29+
* Represents a successful response, which may contain both
30+
* successful and failed events. A Success case indicates the publish
31+
* itself succeeded, not that all events were processed successfully.
32+
*
33+
* @property successfulEvents list of events successfully processed by AWS AppSync.
34+
* @property failedEvents list of events that AWS AppSync failed to process.
35+
* @property status of the publish call.
36+
* Successful = All events published successfully
37+
* Failed = All events failed to publish
38+
* PartialSuccess = Mix of successful and failed events. Check event indexes to determine individual states. */
39+
@Serializable
40+
data class Response internal constructor(
41+
@SerialName("successful") val successfulEvents: List<SuccessfulEvent>,
42+
@SerialName("failed") val failedEvents: List<FailedEvent>
43+
) : PublishResult() {
44+
45+
/**
46+
* Contains identifying information of an event AWS AppSync failed to process.
47+
*/
48+
sealed class Status {
49+
data object Successful : Status()
50+
data object Failed : Status()
51+
data object PartialSuccess : Status()
52+
}
4453

45-
val status: Status
46-
get() {
47-
return when {
54+
val status: Status
55+
get() = when {
4856
successfulEvents.isNotEmpty() && failedEvents.isNotEmpty() -> Status.PartialSuccess
4957
failedEvents.isNotEmpty() -> Status.Failed
5058
else -> Status.Successful
5159
}
52-
}
60+
}
61+
62+
/**
63+
* Represents a failed response where the publish was not successful
64+
*/
65+
data class Failure internal constructor(val error: EventsException) : PublishResult()
5366
}
5467

5568
/**

appsync/aws-appsync-events/src/main/java/com/amplifyframework/aws/appsync/events/utils/Headers.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,20 @@
1515

1616
package com.amplifyframework.aws.appsync.events.utils
1717

18+
import com.amplifyframework.aws.appsync.events.BuildConfig
19+
1820
internal object HeaderKeys {
1921
const val HOST = "host"
2022
const val ACCEPT = "accept"
2123
const val CONTENT_TYPE = "content-type"
2224
const val SEC_WEBSOCKET_PROTOCOL = "Sec-WebSocket-Protocol"
25+
const val USER_AGENT = "User-Agent"
26+
const val X_AMZ_USER_AGENT = "x-amz-user-agent"
2327
}
2428

2529
internal object HeaderValues {
2630
const val ACCEPT_APPLICATION_JSON = "application/json, text/javascript"
2731
const val CONTENT_TYPE_APPLICATION_JSON = "application/json; charset=UTF-8"
2832
const val SEC_WEBSOCKET_PROTOCOL_APPSYNC_EVENTS = "aws-appsync-event-ws"
33+
const val USER_AGENT = "aws-appsync-events-android#${BuildConfig.VERSION_NAME}"
2934
}

0 commit comments

Comments
 (0)