Skip to content

Commit 376450d

Browse files
[store] switch keys from String to (base64 encoded) binary (#28)
1 parent c5be7ac commit 376450d

File tree

14 files changed

+437
-122
lines changed

14 files changed

+437
-122
lines changed

Cargo.lock

Lines changed: 299 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ tracing = "0.1.41"
1313
tracing-subscriber = "0.3.19"
1414
thiserror = "2.0.12"
1515
axum = "0.8.1"
16-
serde = "1.0.219"
1716
base64 = "0.22.1"
1817
rocksdb = "0.22.0"
1918
rand = "0.8.5"
@@ -25,7 +24,9 @@ reqwest = "0.12.5"
2524
tokio-tungstenite = "0.23.1"
2625
url = "2.5.2"
2726
futures-util = "0.3.30"
27+
serde = "1.0.219"
2828
serde_json = "1.0.120"
29+
serde_with = { version = "3.14.0" }
2930
tempfile = "3.10.1"
3031
portpicker = "0.1.1"
3132
http = "1.3.1"

interface.yaml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ paths:
3838
required: true
3939
schema:
4040
type: string
41-
description: The key for the store operation.
41+
format: byte
42+
description: The base64-encoded key for the store operation.
4243
- $ref: '#/components/parameters/tokenQuery'
4344
get:
4445
summary: Get a value
@@ -123,13 +124,15 @@ paths:
123124
required: false
124125
schema:
125126
type: string
126-
description: The key to start the query from (inclusive).
127+
format: byte
128+
description: The key to start the query from (inclusive), base64-encoded.
127129
- name: end
128130
in: query
129131
required: false
130132
schema:
131133
type: string
132-
description: The key to end the query at (exclusive).
134+
format: byte
135+
description: The key to end the query at (exclusive), base64-encoded.
133136
- name: limit
134137
in: query
135138
required: false

sdk-rs/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ futures-util = { workspace = true }
1616
http = { workspace = true }
1717
reqwest = { workspace = true, features = ["json"] }
1818
serde = { workspace = true, features = ["derive"] }
19+
serde_with = { workspace = true, features = ["base64"] }
1920
serde_json = { workspace = true }
2021
thiserror = { workspace = true }
2122
tokio = { workspace = true, features = ["full"] }

sdk-rs/src/store.rs

Lines changed: 24 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,25 @@ use crate::{error::Error, Client as SdkClient};
44
use base64::{engine::general_purpose, Engine as _};
55
use reqwest::header::{HeaderValue, AUTHORIZATION};
66
use serde::{Deserialize, Serialize};
7+
use serde_with::{base64::Base64, serde_as};
78

89
/// The JSON payload for a `get` operation response.
10+
#[serde_as]
911
#[derive(Serialize, Deserialize, Debug)]
1012
pub struct GetResultPayload {
11-
pub value: String,
12-
}
13-
14-
/// The result of a `get` operation.
15-
#[derive(Debug)]
16-
pub struct GetResult {
17-
/// The retrieved value.
13+
#[serde_as(as = "Base64")]
1814
pub value: Vec<u8>,
1915
}
2016

21-
/// An item in the result of a `query` operation. For internal use.
17+
/// An item in the result of a `query` operation.
18+
#[serde_as]
2219
#[derive(Serialize, Deserialize, Debug)]
2320
pub struct QueryResultItemPayload {
24-
pub key: String,
25-
pub value: String,
26-
}
27-
28-
/// An item in the result of a `query` operation.
29-
#[derive(Debug)]
30-
pub struct QueryResultItem {
3121
/// The key of the item.
32-
pub key: String,
22+
#[serde_as(as = "Base64")]
23+
pub key: Vec<u8>,
3324
/// The value of the item.
25+
#[serde_as(as = "Base64")]
3426
pub value: Vec<u8>,
3527
}
3628

@@ -40,13 +32,6 @@ pub struct QueryResultPayload {
4032
pub results: Vec<QueryResultItemPayload>,
4133
}
4234

43-
/// The result of a `query` operation.
44-
#[derive(Debug)]
45-
pub struct QueryResult {
46-
/// A list of key-value pairs.
47-
pub results: Vec<QueryResultItem>,
48-
}
49-
5035
/// A client for interacting with the key-value store.
5136
#[derive(Clone)]
5237
pub struct Client {
@@ -60,8 +45,9 @@ impl Client {
6045
}
6146

6247
/// Sets a key-value pair in the store.
63-
pub async fn set(&self, key: &str, value: Vec<u8>) -> Result<(), Error> {
64-
let url = format!("{}/store/{}", self.client.base_url, key);
48+
pub async fn set(&self, key: &[u8], value: Vec<u8>) -> Result<(), Error> {
49+
let key_b64 = general_purpose::STANDARD.encode(key);
50+
let url = format!("{}/store/{}", self.client.base_url, key_b64);
6551
let mut headers = reqwest::header::HeaderMap::new();
6652
headers.insert(
6753
AUTHORIZATION,
@@ -87,8 +73,9 @@ impl Client {
8773
/// Retrieves a value from the store by its key.
8874
///
8975
/// If the key does not exist, `Ok(None)` is returned.
90-
pub async fn get(&self, key: &str) -> Result<Option<GetResult>, Error> {
91-
let url = format!("{}/store/{}", self.client.base_url, key);
76+
pub async fn get(&self, key: &[u8]) -> Result<Option<GetResultPayload>, Error> {
77+
let key_b64 = general_purpose::STANDARD.encode(key);
78+
let url = format!("{}/store/{}", self.client.base_url, key_b64);
9279
let mut headers = reqwest::header::HeaderMap::new();
9380
headers.insert(
9481
AUTHORIZATION,
@@ -111,10 +98,7 @@ impl Client {
11198
return Err(Error::Http(res.status()));
11299
}
113100

114-
let payload: GetResultPayload = res.json().await?;
115-
let value = general_purpose::STANDARD.decode(payload.value)?;
116-
117-
Ok(Some(GetResult { value }))
101+
Ok(Some(res.json().await?))
118102
}
119103

120104
/// Queries for a range of key-value pairs.
@@ -126,19 +110,21 @@ impl Client {
126110
/// * `limit` - The maximum number of results to return. If `None`, all results are returned.
127111
pub async fn query(
128112
&self,
129-
start: Option<&str>,
130-
end: Option<&str>,
113+
start: Option<&[u8]>,
114+
end: Option<&[u8]>,
131115
limit: Option<usize>,
132-
) -> Result<QueryResult, Error> {
116+
) -> Result<QueryResultPayload, Error> {
133117
let mut url = format!("{}/store?", self.client.base_url);
134118
if let Some(start) = start {
135-
url.push_str(&format!("start={}&", start));
119+
let start_b64 = general_purpose::STANDARD.encode(start);
120+
url.push_str(&format!("start={start_b64}&"));
136121
}
137122
if let Some(end) = end {
138-
url.push_str(&format!("end={}&", end));
123+
let end_b64 = general_purpose::STANDARD.encode(end);
124+
url.push_str(&format!("end={end_b64}&"));
139125
}
140126
if let Some(limit) = limit {
141-
url.push_str(&format!("limit={}", limit));
127+
url.push_str(&format!("limit={limit}"));
142128
}
143129

144130
let mut headers = reqwest::header::HeaderMap::new();
@@ -160,14 +146,7 @@ impl Client {
160146
}
161147

162148
let payload: QueryResultPayload = res.json().await?;
163-
let mut results = Vec::new();
164-
for item in payload.results {
165-
results.push(QueryResultItem {
166-
key: item.key,
167-
value: general_purpose::STANDARD.decode(item.value)?,
168-
});
169-
}
170149

171-
Ok(QueryResult { results })
150+
Ok(payload)
172151
}
173152
}

sdk-ts/__tests__/sdk.test.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe('Exoware TS SDK', () => {
2020
describe('StoreClient', () => {
2121
it('should set and get a value', async () => {
2222
const store = client.store();
23-
const key = 'test-key';
23+
const key = new TextEncoder().encode('test-key');
2424
const value = Buffer.from('test-value');
2525

2626
await store.set(key, value);
@@ -32,24 +32,25 @@ describe('Exoware TS SDK', () => {
3232

3333
it('should return null for a non-existent key', async () => {
3434
const store = client.store();
35-
const result = await store.get('non-existent-key');
35+
const result = await store.get(new TextEncoder().encode('non-existent-key'));
3636
expect(result).toBeNull();
3737
});
3838

3939
it('should query for key-value pairs', async () => {
4040
const store = client.store();
41+
const encoder = new TextEncoder();
4142
const prefix = 'query-test-';
4243
const pairs = [
43-
{ key: `${prefix}a`, value: Buffer.from('a') },
44-
{ key: `${prefix}b`, value: Buffer.from('b') },
45-
{ key: `${prefix}c`, value: Buffer.from('c') },
44+
{ key: encoder.encode(`${prefix}a`), value: Buffer.from('a') },
45+
{ key: encoder.encode(`${prefix}b`), value: Buffer.from('b') },
46+
{ key: encoder.encode(`${prefix}c`), value: Buffer.from('c') },
4647
];
4748

4849
for (const pair of pairs) {
4950
await store.set(pair.key, pair.value);
5051
}
5152

52-
const result = await store.query(`${prefix}a`, `${prefix}z`);
53+
const result = await store.query(encoder.encode(`${prefix}a`), encoder.encode(`${prefix}z`));
5354
expect(result.results.length).toBe(3);
5455
expect(result.results.map(r => Buffer.from(r.value))).toEqual(pairs.map(p => p.value));
5556
expect(result.results.map(r => r.key).sort()).toEqual(pairs.map(p => p.key).sort());
@@ -58,7 +59,7 @@ describe('Exoware TS SDK', () => {
5859
describe('limits', () => {
5960
it('should handle key at size limit', async () => {
6061
const store = client.store();
61-
const key = 'a'.repeat(512);
62+
const key = new TextEncoder().encode('a'.repeat(512));
6263
const value = Buffer.from('test-value');
6364

6465
await store.set(key, value);
@@ -70,15 +71,15 @@ describe('Exoware TS SDK', () => {
7071

7172
it('should reject key over size limit', async () => {
7273
const store = client.store();
73-
const key = 'a'.repeat(513);
74+
const key = new TextEncoder().encode('a'.repeat(513));
7475
const value = Buffer.from('test-value');
7576

7677
await expect(store.set(key, value)).rejects.toThrow('HTTP error: 413 Payload Too Large');
7778
});
7879

7980
it('should handle value at size limit', async () => {
8081
const store = client.store();
81-
const key = 'value_at_limit';
82+
const key = new TextEncoder().encode('value_at_limit');
8283
const value = Buffer.alloc(20 * 1024 * 1024);
8384

8485
await store.set(key, value);
@@ -90,7 +91,7 @@ describe('Exoware TS SDK', () => {
9091

9192
it('should reject value over size limit', async () => {
9293
const store = client.store();
93-
const key = 'value_over_limit';
94+
const key = new TextEncoder().encode('value_over_limit');
9495
const value = Buffer.alloc(20 * 1024 * 1024 + 1);
9596

9697
await expect(store.set(key, value)).rejects.toThrow('HTTP error: 413 Payload Too Large');

sdk-ts/src/store.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export interface GetResult {
1616
*/
1717
export interface QueryResultItem {
1818
/** The key of the item. */
19-
key: string;
19+
key: Uint8Array;
2020
/** The value of the item. */
2121
value: Uint8Array;
2222
}
@@ -40,8 +40,9 @@ export class StoreClient {
4040
* @param key The key to set.
4141
* @param value The value to set.
4242
*/
43-
async set(key: string, value: Uint8Array | Buffer): Promise<void> {
44-
const url = `${this.client.baseUrl}/store/${key}`;
43+
async set(key: Uint8Array, value: Uint8Array | Buffer): Promise<void> {
44+
const encodedKey = Base64.fromUint8Array(key);
45+
const url = `${this.client.baseUrl}/store/${encodedKey}`;
4546
try {
4647
await this.client.httpClient.post(url, value, {
4748
headers: { 'Content-Type': 'application/octet-stream' },
@@ -59,8 +60,9 @@ export class StoreClient {
5960
* @param key The key to retrieve.
6061
* @returns The value, or `null` if the key does not exist.
6162
*/
62-
async get(key: string): Promise<GetResult | null> {
63-
const url = `${this.client.baseUrl}/store/${key}`;
63+
async get(key: Uint8Array): Promise<GetResult | null> {
64+
const encodedKey = Base64.fromUint8Array(key);
65+
const url = `${this.client.baseUrl}/store/${encodedKey}`;
6466
try {
6567
const response = await this.client.httpClient.get<{ value: string }>(url);
6668
const value = Base64.toUint8Array(response.data.value);
@@ -82,17 +84,23 @@ export class StoreClient {
8284
* @param end The key to end the query at (exclusive). If `undefined`, the query continues to the last key.
8385
* @param limit The maximum number of results to return. If `undefined`, all results are returned.
8486
*/
85-
async query(start?: string, end?: string, limit?: number): Promise<QueryResult> {
87+
async query(start?: Uint8Array, end?: Uint8Array, limit?: number): Promise<QueryResult> {
8688
const url = new URL(`${this.client.baseUrl}/store`);
87-
if (start) url.searchParams.append('start', start);
88-
if (end) url.searchParams.append('end', end);
89+
if (start) {
90+
const encodedStart = Base64.fromUint8Array(start);
91+
url.searchParams.append('start', encodedStart);
92+
}
93+
if (end) {
94+
const encodedEnd = Base64.fromUint8Array(end);
95+
url.searchParams.append('end', encodedEnd);
96+
}
8997
if (limit) url.searchParams.append('limit', limit.toString());
9098

9199
try {
92100
const response = await this.client.httpClient.get<{ results: { key: string, value: string }[] }>(url.toString());
93101
const results = response.data.results.map(item => ({
94-
key: item.key,
95-
value: Base64.toUint8Array(item.value),
102+
key: Base64.toUint8Array(item.key),
103+
value: Base64.toUint8Array(item.value)
96104
}));
97105
return { results };
98106
} catch (error) {

simulator/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ tracing-subscriber = { workspace = true }
1919
thiserror = { workspace = true }
2020
axum = { workspace = true, features = ["ws"] }
2121
serde = { workspace = true, features = ["derive"] }
22+
serde_with = { workspace = true }
2223
base64 = { workspace = true }
2324
rocksdb = { workspace = true }
2425
rand = { workspace = true }

simulator/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ async fn main() -> std::process::ExitCode {
3939
// Initialize the default directory for the persistent store. This will be
4040
// `$HOME/.exoware_simulator`.
4141
let home_directory = std::env::var("HOME").expect("$HOME is not configured");
42-
let default_directory = PathBuf::from(format!("{}/.exoware_simulator", home_directory));
42+
let default_directory = PathBuf::from(format!("{home_directory}/.exoware_simulator"));
4343
let default_directory: &'static str = default_directory.to_str().unwrap().to_string().leak();
4444

4545
// Define the CLI application and its arguments.

simulator/src/server/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ pub async fn run(
5757
);
5858

5959
// Create a listener for the server on the specified port.
60-
let listener = TcpListener::bind(format!("0.0.0.0:{}", port)).await?;
60+
let listener = TcpListener::bind(format!("0.0.0.0:{port}")).await?;
6161
info!(address = %listener.local_addr()?, "server listening");
6262

6363
// Create a router for the server.

0 commit comments

Comments
 (0)