Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion Sources/Valkey/Documentation.docc/Pipelining.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
Sending multiple commands at once without waiting for the response of each command.

## Overview
=======

Valkey pipelining is a technique for improving performance by issuing multiple commands at once without waiting for the response to each individual command. Pipelining not only reduces the latency cost of waiting for the result of each command it also reduces the cost to the server as it reduces I/O costs. Multiple commands can be read with a single syscall, and multiple results are delivered with a single syscall.

Expand All @@ -23,6 +22,27 @@ if let result = try getResult.get().map({ String(buffer: $0) }) {
}
```

### Dynamic pipelines

The parameter pack implementation of pipelining allows for creation of static pipelines built at compile time. It doesn't provide much scope for generating more dynamic pipelines based on runtime conditions. To get around this an API that takes an array of existential `ValkeyCommands` and returns an array of `Result<RESPToken, Error>` is available. It allows you to build your pipeline at runtime. The downside of this method is you are returned a `Result` holding a ``RESPToken`` which needs decoding.

```swift
// create command array
var commands: [any ValkeyCommand] = []
commands.append(SET("foo", value: "100"))
commands.append(INCR("foo"))
commands.append(GET("foo"))
// execute commands
let results = await valkeyClient.execute(commands)
// get result and decode. We decode as an optional String
// to avoid an error being thrown if the response is a null token
if let value = results[2].get().decode(as: String?.self) {
print(value)
}
```

You can find out more about decoding `RESPToken` in <doc:RESPToken-Decoding>.

### Pipelining and Concurrency

Being able to have multiple requests in transit on a single connection means we can have multiple tasks use that connection concurrently. Each request is added to a queue and as each response comes back the first request on the queue is popped off and given the response. By using a single connection across multiple tasks you can reduce the number of connections to your database.
Expand Down
69 changes: 69 additions & 0 deletions Sources/Valkey/Documentation.docc/RESPToken-Decoding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Decoding RESP

Decoding the contents of RESPToken

## Overview

The wire protocol valkey-swift uses is RESP3. It is simple, human readable and a fast protocol to parse. It can be used to serialize many different types including strings, integers, doubles, array and maps. More can be found out about RESP in the [Valkey Documentation](https://valkey.io/topics/protocol/).

We represent a raw RESP token using the type ``RESPToken``. A parsed RESP value is represented by the enum ``RESPToken/Value``. This includes cases for the different datatypes a RESP token can represent.

The majority of the Valkey commands return the Swift types equivalent to their expected response. eg `GET` returns a `ByteBuffer` containing the contents of the key, `STRLEN` returns the length of the contents as an `Int`. But there are a number of reasons for commands to not have a defined return type and in these cases a command may return the type ``RESPToken`` or one of the sequence types ``RESPToken/Array`` or ``RESPToken/Map``.

## Decoding RESPToken

A `RESPToken` contains the raw serialized bytes returned by the Valkey server. Valkey-swift introduces a protocol ``RESPTokenDecodable`` for types that can be decoded from a `RESPToken`. Many of Swift core types have been extended to conform to `RESPTokenDecodable`. There are two ways to decode a `RESPToken`. You can call ``RESPTokenDecodable/init(fromRESP:)``.

```swift
let string = String(fromRESP: respToken)
```

Or you can call the `RESPToken` method ``RESPToken/decode(as:)``. This can be chained onto the end of a command call eg `RPOP` can return a single value or an array of values so the function returns a `RESPToken` and the user should decode it based on whether they asked for multiple or a single value to be popped.

```swift
let string = try await valkeyClient.rpop("myList")?.decode(as: String.self)
```

## Decoding RESPToken.Array

When a command returns an array it is returned as an ``RESPToken/Array``. This can be to avoid the additional memory allocation of creating a Swift `Array` or because the array represents a more complex type. `RESPToken.Array` conforms to `Sequence` and its element type is a `RESPToken`. You can iterate over its contents and decode each element as follows.

```swift
let values = try await valkeyClient.smembers("mySet")
for value in values {
let string = value.decode(as: String.self)
print(string)
}
```

Alternatively if you don't mind the additional allocation you can decode as a Swift `Array`

```swift
let values = try await valkeyClient.smembers("mySet").decode(as: [String].self)
```

The type of each element of a `RESPToken.Array` is not fixed. It is possible for values in the same array to represent different types. Decoding different types from an array is done using either `RESPToken.Array` method ``RESPToken/Array/decodeElements(as:)`` or `RESPToken` method ``RESPToken/decodeArrayElements(as:)``. The following code decodes the first element of an array as a `String` and the second as an `Int`.

```swift
let (member, score) = respToken.decodeArrayElements(as: (String, Int).self)
```

## Decoding RESPToken.Map

When a command returns a dictionary it is returned as a ``RESPToken/Map``. This can be to avoid the additional memory allocation of creating a Swift `Dictionary`, or a more complex type is being represented. `RESPToken.Map` conforms to `Sequence` and its element type is a key value pair of two `RESPToken`. You can iterate over its contents and decode its elements as follows.

```swift
let values = try await client.configGet(parameters: ["*max-*-entries*"])
for (keyToken, valueToken) in values {
let key = try keyToken.decode(as: String.self)
let value = try valueToken.decode(as: String.self)
...
}
```

Alternatively if you don't mind the additional allocation you can decode as a Swift `Dictionary`

```swift
let values = try await client.configGet(parameters: ["*max-*-entries*"])
.decode(as: [String: String].self)
```
6 changes: 6 additions & 0 deletions Sources/Valkey/Documentation.docc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Valkey-swift is a swift based client for Valkey, the high-performance key/value

- <doc:getting-started>
- <doc:Pipelining>
- <doc:RESPToken-Decoding>
- <doc:Pubsub>
- <doc:Transactions>

Expand All @@ -23,6 +24,7 @@ Valkey-swift is a swift based client for Valkey, the high-performance key/value
- ``ValkeyServerAddress``
- ``ValkeyConnection``
- ``ValkeyConnectionConfiguration``
- ``ValkeyTracingConfiguration``

### Commands

Expand All @@ -42,10 +44,14 @@ Valkey-swift is a swift based client for Valkey, the high-performance key/value

- ``ValkeySubscription``
- ``ValkeySubscriptionMessage``
- ``ValkeySubscribeCommand``
- ``ValkeySubscriptionFilter``

### Errors

- ``ValkeyClientError``
- ``ValkeyClusterError``
- ``RESPDecodeError``
- ``RESPParsingError``

### Cluster
Expand Down
1 change: 1 addition & 0 deletions Sources/Valkey/RESP/RESPTokenDecodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import NIOCore

/// A type that can decode from a response token.
public protocol RESPTokenDecodable {
/// Initialize from RESPToken
init(fromRESP: RESPToken) throws
}

Expand Down