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
1 change: 1 addition & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ The SDK includes a resilient caching layer that wraps service methods.
- **InMemoryCacheAdapter**: A default, zero-dependency in-memory cache.
- **Resilience**: Caching is non-blocking and failure-tolerant. Cache errors are isolated from the main request flow.
- **Observability**: Developers can monitor cache health via lifecycle hooks.
- **Key format**: See [Cache Adapters → Key Composition](./cache-adapters.md#key-composition) for the exact cache key templates and [TTL Precedence](./cache-adapters.md#ttl-precedence) for expiry behaviour.

**In-flight request coalescing.** When `deduplication` is enabled (the
default), concurrent calls with identical arguments share a single
Expand Down
64 changes: 63 additions & 1 deletion docs/cache-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,36 @@ interface CacheAdapter {
- `invalidateWalletCache()` falls back to clearing the **entire** cache.
- **Must never throw.**

## Key Composition

Every cached service method builds a deterministic cache key from public identifiers. All wallet addresses are normalised to lowercase via `normaliseAddress()` before key construction. No secrets (API keys, tokens) are included in cache keys.

| Service method | Cache key template |
| :------------- | :----------------- |
| `access.checkAccess` | `access:checkAccess:{guildId}:{resourceId}:{wallet}` |
| `access.checkRoleAccess` | `access:checkRoleAccess:{guildId}:{roleId}:{wallet}` |
| `membership.getMembership` | `membership:getMembership:{guildId}:{wallet}` |
| `roles.getRoles` | `roles:getRoles:{guildId}` |
| `roles.getUserRoles` | `roles:getUserRoles:{guildId}:{wallet}` |
| `guilds.getGuild` | `guilds:getGuild:{guildId}` |
| `guilds.getGuildConfig` | `guilds:getGuildConfig:{guildId}` |

> The `wallet:` prefix (e.g. `wallet:0x1234:`) is an **invalidation-only namespace** used by `invalidateWalletCache()`. No service method produces a standalone `wallet:*` cache key — wallet addresses are always embedded within the service-method key templates above.

**Concrete example:**

```typescript
// Given:
// guildId = 'prime-guild'
// resourceId = 'secret-channel'
// walletAddress = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'
//
// cache key produced:
// access:checkAccess:prime-guild:secret-channel:0xd8da6bf26964af9d7eed9e03e53415d37aa96045
```

> **Tip for custom adapter authors:** Use these exact key templates to design prefix-based invalidation strategies (e.g., scan for `access:checkAccess:{guildId}:*` to evict all access entries for a guild).

## TTL Semantics

| `ttl` parameter | Behaviour |
Expand All @@ -58,7 +88,37 @@ interface CacheAdapter {
> `cacheTtl` (or pass `undefined`) — only an _absent_ TTL means "no
> expiration."

The SDK passes `cacheTtl` (from client config) as the `ttl` argument. Methods that accept a per-call override subtract elapsed time from the deadline before storing.
The SDK passes the client-level `cacheTtl` as the `ttl` argument to `cache.set()`. Every cached service method uses the same client-wide TTL — there is **no per-call TTL override** in the current implementation.

### TTL Precedence

Only one TTL source exists: the `cacheTtl` option passed to `GuildPassClient` at construction time.

| `cacheTtl` value | Effective behaviour |
| :--------------- | :------------------ |
| `undefined` (omitted) | Never expire (equivalent to `0`). |
| `0` | Never expire. |
| `> 0` | All cached entries expire after `cacheTtl` milliseconds. |

**Worked example:**

```typescript
const client = new GuildPassClient({
apiUrl: 'https://api.guildpass.xyz',
cache: new InMemoryCacheAdapter(),
cacheTtl: 60_000, // 60 seconds
});

// First call — network request; cached with 60 s TTL
await client.guilds.getGuild({ guildId: 'prime-guild' });

// Second call within 60 s — cache hit; no network request
await client.guilds.getGuild({ guildId: 'prime-guild' });

// After 60 s — entry expired; next call goes to network and re-caches
```

All service methods (`checkAccess`, `checkRoleAccess`, `getMembership`, `getRoles`, `getUserRoles`, `getGuild`, `getGuildConfig`) share this same TTL. There is no way to set a different TTL per method or per call.

## Error Isolation

Expand Down Expand Up @@ -303,6 +363,8 @@ await client.clearCache();

See the [SDK Guide](./sdk-guide.md#caching-and-request-deduplication) for more on the caching layer.

For the internal cache-wrapping layer that produces these keys, see [Architecture → Caching Layer](./architecture.md#5-caching-layer).

## Conformance Testing

Custom adapters should run the exported conformance suite. It covers value
Expand Down
2 changes: 1 addition & 1 deletion docs/sdk-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@ const client = new GuildPassClient({

The hook receives a `CacheErrorHookPayload` containing the operation name (`get`, `set`, `delete`, `clear`), the affected `key` (if any), and the original `error`.

For full details on implementing custom cache adapters — including TTL semantics, `deleteByPrefix`, serialisation, and production examples — see the [Cache Adapters Guide](./cache-adapters.md).
For full details on implementing custom cache adapters — including TTL semantics, `deleteByPrefix`, serialisation, production examples, and the exact [cache key composition](./cache-adapters.md#key-composition) per service method — see the [Cache Adapters Guide](./cache-adapters.md).

### Security Note

Expand Down