Skip To Content

Client API

Work with typed caches, expiration, and retries.

squirix exposes a typed gRPC client SDK. Cache operations use gRPC only; HTTP endpoints are limited to health and metrics.

Client SDK

Entry point (Squirix.Client):

using System;
using System.Threading;
using Squirix.Client;

await using var client = await SquirixClient.ConnectAsync(new Uri("https://localhost:5001"), CancellationToken.None);
var cache = await client.GetCacheAsync<T>("cache-name", CancellationToken.None);

ICache<T> methods (v0.1 exported surface):

Method Purpose
AddAsync / TryAddAsync Insert if absent
SetAsync Upsert with optional expiration
UpdateAsync Update existing entry
GetValueAsync / GetEntryAsync Read with explicit presence
GetExpirationAsync Read expiration metadata
GetOrAddAsync Read or insert factory value
RemoveAsync Delete key
TouchAsync / RemoveExpirationAsync Expiration management

Prefer GetValueAsync for reads with explicit presence.

Writes accept (key, value, options?, cancellationToken). Expiration uses CacheEntryOptions, not CacheEntry<T> write overloads.

When options is omitted or null, or neither CacheEntryOptions.Expiration nor CacheEntryOptions.ExpiresAt is set, the entry is stored without expiration and does not expire by TTL. Pass an explicit relative or absolute expiration when you need TTL eviction.

An absolute expiration (CacheEntryOptions.ExpiresAt, TouchAsync(key, DateTimeOffset)) is on the client clock. The client sends the time left before it, so the entry lives that long even when the client and server clocks disagree. A value that is not in the future on the client clock is rejected with ArgumentOutOfRangeException. GetEntryAsync reports ExpiresUtc on the client clock too: the server returns the time the entry has left, and the client adds it to its own time.

SetAsync overwrites the whole entry, including its expiration: a SetAsync without expiration on a key that already has a TTL clears that TTL, and the key no longer expires. To change only the value and keep the current expiration, use UpdateAsync.

Out of scope for v0.1: batch, scan, watch, counters, tag invalidation, compare-and-set.

Configuration (SquirixClientOptions): endpoints, JWT bearer token provider, custom serializer. See configuration.md and serialization.md.

Wire contract

gRPC contract: src/shared/Squirix/Transport/Grpc/Protos/SquirixCache.proto (shared source, not a separate NuGet package).

SquirixCacheService exposes 10 unary RPCs on the v0.1 server surface:

Wire RPC (SquirixCache.proto) gRPC client / ICache<T> Notes
SetEntry SetAsync Upsert via CacheEntryWire (SetEntryAsync on generated client)
TryAddEntry TryAddAsync / AddAsync Insert-if-absent via CacheEntryWire (TryAddEntryAsync on generated client); AddAsync throws when the key exists
GetValue GetValueAsync Value-only read; returns found + value
GetEntry GetEntryAsync Full entry read; returns found + entry (missing key → found=false, not an error) and the time left (remaining)
GetExpiration GetExpirationAsync Expiration metadata only; handler reads via runtime GetEntry
GetOrAdd GetOrAddAsync Single RPC with CacheEntryWire; client runs factory locally, server get-or-insert atomically; no value on add
Update UpdateAsync Update value if key exists via CacheEntryWire (value field only)
Remove RemoveAsync
Touch TouchAsync Relative expiration (Duration)
RemoveExpiration RemoveExpirationAsync

Mutations that accept expiration use SetEntry / TryAddEntry with CacheEntryWire. There are no flat Set / TryAdd value-only mutation RPCs on the wire surface.

The server runtime pipeline (ICacheApi) is entry-based only (eight methods). gRPC handlers translate wire requests into that runtime surface; GetExpiration and GetOrAdd are wire convenience RPCs whose handlers may compose runtime calls internally — the client SDK calls one RPC per exported method and does not stitch multiple RPCs together.

Wire RPC names omit the Async suffix; grpc-dotnet appends it on generated client methods (for example SetEntry → SetEntryAsync). Public ICache<T> names stay SetAsync / TryAddAsync.

There is no Contains RPC. Prefer GetValueAsync or GetEntryAsync for presence checks (found=false is not an error). HTTP on the primary listener is limited to health and metrics — there is no REST cache HEAD route in v0.1.

Value encoding

Every service lives in the proto package squirix.cache.v2, so a client and a server that disagree on the value encoding meet as Unimplemented instead of silently corrupting values. A CacheValue is one of:

Kind Used for
null_value null
string_value string
int64_value int and long
double_value double
bool_value bool
json_utf8 everything else: objects, arrays, decimal, numbers of a JSON element

json_utf8 holds exactly one JSON value as UTF-8: no byte order mark, valid UTF-8, no comments, no trailing commas, nesting of at most 64 levels. The server checks it and refuses anything else with InvalidArgument before any durable effect, then stores, journals, replicates and returns the very same bytes: it does not parse, reformat or canonicalize the JSON, so every digit of a number survives. A Remove response carries only removed. A GetOrAdd response carries the value only when the key already held one (added=false, found=true); when it added, the caller already has the value.

Mutating gRPC RPCs require a non-empty operation_id of exactly 32 lowercase hex characters (UUID without hyphens, for example 0123456789abcdef0123456789abcdef). The Squirix client SDK generates a fresh id per mutating call via RpcOperationIdentity.New(); custom gRPC clients must supply a conforming value. Over-length or malformed ids are rejected with gRPC InvalidArgument. The server deduplicates retries with the same operation_id and rejects reuse with a different payload (FailedPrecondition).

Entry limits (v0.1)

Limit Value
Encoded entry payload 4 MiB
Tags per entry 32
Tag key UTF-8 size 256 bytes
Tag value UTF-8 size 1024 bytes

Violations return stable public codes: INVALID_ENTRY_TAGS as gRPC InvalidArgument, and PAYLOAD_TOO_LARGE as gRPC ResourceExhausted. Other live public codes include INVALID_CACHE_KEY, TOO_MANY_REQUESTS, MEMORY_PRESSURE, JOURNAL_DISK_QUOTA, and the OPERATION_ID_* family. Cache names still fail as ArgumentException → gRPC InvalidArgument without a structured INVALID_CACHE_NAME code.

Request Routing

In a multi-node cluster, the entry node forwards the client request to the key owner (the replica group leader when automatic failover is on) over internode gRPC unchanged, with the client operation_id and without running the cache pipeline itself.

Only the owner records the outcome: idempotency records are per-node in memory (durable nodes also persist outcomes through the journal/snapshot path), and the entry node keeps none.

Retry Behavior

When a retry lands on a different entry node (bootstrap endpoint switch or transport failover), the owner node replays the cached outcome for the same operation_id and fingerprint so the mutation is not applied twice.

The owner response and errors reach the client unchanged; an unreachable owner surfaces as Unavailable.

Replay Windows

  • In a replicated cache an outcome is replayable for at least 30 seconds by the clock of the node that answers the retry; with node clocks within 10 seconds of each other, it is replayable for at least 20 seconds after the leader decided it, across a leader change or a restart too.

  • The squirix client retries within its 15 s operation deadline, so it is covered.

  • A raw gRPC client that retries later than that may get a new execution of the mutation.

  • On an RF=1 node an outcome is replayable for at least 30 seconds, and up to 15 minutes while the byte budget of the store has room.

Conditional Writes

A replicated host dedupes every write in the replica group of its key and keeps no node-wide record of it.

There, a conditional write that took no effect (TryAddEntry or GetOrAdd over a present key, Update of an absent key) is evaluated again on retry instead of replayed, and the read of GetOrAdd reflects the current value.

Store Capacity

Operators can tune the in-memory store caps of an RF=1 node with SQUIRIX_IDEMPOTENCY_* — see configuration.md — Idempotency store capacity.

RPC Surface

The approved RPC list is locked by a golden snapshot test: tests/squirix.server/squirix.server.unit-tests/ApiSnapshots/SquirixGrpcEndpointSurface.golden.txt.

Transport And Authentication

Transport requires HTTPS endpoints. Cleartext http:// URLs are rejected at configuration time.

Authentication uses JWT bearer tokens when enabled via SquirixClientOptions.BearerTokenProvider.

Cache names

Validation rules for logical cache names: cache-name-validation.md.