first commit
This commit is contained in:
78
docs/foundation/flow/idempotency/src/store/database-store.md
Normal file
78
docs/foundation/flow/idempotency/src/store/database-store.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# `database-store`
|
||||
|
||||
## Purpose
|
||||
|
||||
Provides a persistent `IdempotencyStoreAdapter<T>` backed by any relational database via the `IdempotencyDatabaseAdapter` interface. Stores idempotency records in a single flat table with JSON-serialised response columns. Auto-creates the table on first use when `autoCreate` is `true` (default), making zero-setup integration painless.
|
||||
|
||||
## Features
|
||||
|
||||
- Wraps any database that implements `IdempotencyDatabaseAdapter` (SQLite, PostgreSQL, MySQL, etc.)
|
||||
- Auto-creates `idempotency_records` table on first use (configurable)
|
||||
- Upsert semantics: `set` inserts or updates existing rows correctly
|
||||
- JSON serialisation of arbitrary response payloads
|
||||
- `cleanup(beforeTimestamp)` deletes expired rows and returns the removed count
|
||||
- Generic `<T>` parameter for typed response payloads
|
||||
- Injectable table name for multi-tenant or schema-isolated deployments
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { DatabaseIdempotencyStore } from "@suaveplan/idempotency/store";
|
||||
import type { IdempotencyDatabaseAdapter } from "@suaveplan/idempotency/store";
|
||||
|
||||
declare const myDbAdapter: IdempotencyDatabaseAdapter;
|
||||
|
||||
const store = new DatabaseIdempotencyStore({ adapter: myDbAdapter });
|
||||
await store.set("order-123", {
|
||||
key: "order-123", status: "completed", response: { id: 1 },
|
||||
createdAt: Date.now(), updatedAt: Date.now(), expiresAt: Date.now() + 86400_000,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { DatabaseIdempotencyStore } from "@suaveplan/idempotency/store";
|
||||
|
||||
const store = new DatabaseIdempotencyStore({
|
||||
adapter: pgAdapter,
|
||||
tableName: "payments.idempotency",
|
||||
autoCreate: false, // table managed by migrations
|
||||
});
|
||||
|
||||
// Periodic cleanup of expired records
|
||||
setInterval(() => store.cleanup(Date.now()), 60_000);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `new DatabaseIdempotencyStore<T>(config: DatabaseIdempotencyStoreConfig)`
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `adapter` | `IdempotencyDatabaseAdapter` | required | Database client adapter |
|
||||
| `tableName` | `string` | `"idempotency_records"` | Table name |
|
||||
| `autoCreate` | `boolean` | `true` | Auto-create table if absent |
|
||||
|
||||
### `get(key): Promise<IdempotencyRecord<T> | undefined>`
|
||||
|
||||
Queries the table for the given key. Returns `undefined` if not found.
|
||||
|
||||
### `set(key, record): Promise<void>`
|
||||
|
||||
Upserts the record: updates if key exists, inserts otherwise.
|
||||
|
||||
### `delete(key): Promise<boolean>`
|
||||
|
||||
Deletes the row; returns `true` if a row was removed.
|
||||
|
||||
### `cleanup(beforeTimestamp): Promise<number>`
|
||||
|
||||
Deletes all rows with `expires_at < beforeTimestamp`. Returns the count removed.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `ensureTable` runs once per instance (guarded by `initialized` flag); it is idempotent and can be disabled by setting `autoCreate: false`.
|
||||
- JSON serialisation uses `JSON.parse` / `JSON.stringify`; circular references or non-serializable values will throw at `set` time.
|
||||
- The `protected` visibility on `adapter`, `tableName`, `rowToRecord`, and `ensureTable` allows subclasses to extend without full re-implementation.
|
||||
- No connection pooling or transaction management is performed — that responsibility belongs to the injected adapter.
|
||||
85
docs/foundation/flow/idempotency/src/store/memory-store.md
Normal file
85
docs/foundation/flow/idempotency/src/store/memory-store.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# `memory-store`
|
||||
|
||||
## Purpose
|
||||
|
||||
Provides an in-memory implementation of `IdempotencyStoreAdapter<T>` backed by a `Map`. It is the default store for unit tests, local development, and single-process deployments that do not need persistence across restarts. Records are lazily expired on `get` and can be batch-pruned via `cleanup`.
|
||||
|
||||
## Features
|
||||
|
||||
- O(1) `get` / `set` / `delete` via a `Map` keyed by idempotency key
|
||||
- Lazy TTL expiration: expired records are detected and removed on `get`
|
||||
- `cleanup(beforeTimestamp)` for batch removal of expired records
|
||||
- Injectable `clock` function for deterministic testing with `FakeClock`
|
||||
- `size` getter and `clear()` for test assertions and store resets
|
||||
- Generic `<T>` type parameter propagated through stored records
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { MemoryIdempotencyStore } from "@suaveplan/idempotency/store";
|
||||
import { IdempotencyGuard } from "@suaveplan/idempotency/guard";
|
||||
|
||||
const store = new MemoryIdempotencyStore();
|
||||
const guard = new IdempotencyGuard({ store, ttlMs: 3600_000 });
|
||||
|
||||
const result = await guard.execute("key-1", () => fetchUser(1));
|
||||
console.log(store.size); // 1
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { MemoryIdempotencyStore } from "@suaveplan/idempotency/store";
|
||||
|
||||
// Inject a fake clock for deterministic TTL testing
|
||||
let now = 1_000_000;
|
||||
const fakeClock = () => now;
|
||||
const store = new MemoryIdempotencyStore(fakeClock);
|
||||
|
||||
await store.set("k", {
|
||||
key: "k", status: "completed", response: 42,
|
||||
createdAt: now, updatedAt: now, expiresAt: now + 1000,
|
||||
});
|
||||
|
||||
// Advance clock past TTL
|
||||
now += 5000;
|
||||
const result = await store.get("k"); // undefined — expired
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `new MemoryIdempotencyStore<T>(clock?: Clock)`
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `clock` | `() => number` | `Date.now` | Monotonic clock for TTL checks |
|
||||
|
||||
### `get(key): Promise<IdempotencyRecord<T> | undefined>`
|
||||
|
||||
Returns the record if it exists and has not expired. Deletes and returns `undefined` for expired records.
|
||||
|
||||
### `set(key, record): Promise<void>`
|
||||
|
||||
Upserts a record unconditionally.
|
||||
|
||||
### `delete(key): Promise<boolean>`
|
||||
|
||||
Removes the record; returns `true` if it existed.
|
||||
|
||||
### `cleanup(beforeTimestamp): Promise<number>`
|
||||
|
||||
Iterates all stored records and removes those whose `expiresAt <= beforeTimestamp`. Returns the count removed.
|
||||
|
||||
### `size: number`
|
||||
|
||||
Current number of records in the map, including expired entries not yet pruned.
|
||||
|
||||
### `clear(): void`
|
||||
|
||||
Immediately removes all records.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The `clock` is injected at construction time rather than captured from `Date.now` directly, enabling fully deterministic unit tests without patching globals.
|
||||
- Lazy expiration keeps `get` at O(1) while `cleanup` is O(n) — call it periodically in long-running processes.
|
||||
- The `protected` visibility on `records` and `clock` allows subclasses to extend without re-implementing the core logic.
|
||||
76
docs/foundation/flow/idempotency/src/store/redis-store.md
Normal file
76
docs/foundation/flow/idempotency/src/store/redis-store.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# `redis-store`
|
||||
|
||||
## Purpose
|
||||
|
||||
Provides a Redis-backed `IdempotencyStoreAdapter<T>` with automatic key expiration using Redis native TTL. By accepting a generic `RedisClientAdapter` interface rather than a concrete library, this module integrates with `ioredis`, `redis`, Upstash Redis, or any compatible client without coupling to a specific driver.
|
||||
|
||||
## Features
|
||||
|
||||
- JSON-serialised records stored as Redis string values
|
||||
- Automatic TTL set on every `set()` call based on `record.expiresAt`
|
||||
- Fallback TTL (`defaultTtlSeconds`) for records whose expiry is in the past
|
||||
- Configurable key prefix for namespace isolation (multi-tenant)
|
||||
- `cleanup()` scans expired records server-side for manual purging
|
||||
- Generic `<T>` type parameter for typed payloads
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { RedisIdempotencyStore } from "@suaveplan/idempotency/store";
|
||||
import type { RedisClientAdapter } from "@suaveplan/idempotency/store";
|
||||
|
||||
declare const redisAdapter: RedisClientAdapter; // your ioredis wrapper
|
||||
|
||||
const store = new RedisIdempotencyStore({
|
||||
client: redisAdapter,
|
||||
prefix: "app:idempotency:",
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { RedisIdempotencyStore } from "@suaveplan/idempotency/store";
|
||||
|
||||
const store = new RedisIdempotencyStore({
|
||||
client: redisAdapter,
|
||||
prefix: `tenant:${tenantId}:idempotency:`,
|
||||
defaultTtlSeconds: 3600, // 1 hour fallback
|
||||
});
|
||||
|
||||
// Manual cleanup (most records expire via Redis TTL automatically)
|
||||
const removed = await store.cleanup(Date.now());
|
||||
console.log(`Manually removed ${removed} stale records`);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `new RedisIdempotencyStore<T>(config: RedisIdempotencyStoreConfig)`
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `client` | `RedisClientAdapter` | required | Redis client adapter |
|
||||
| `prefix` | `string` | `"idempotency:"` | Key namespace prefix |
|
||||
| `defaultTtlSeconds` | `number` | `86400` | Fallback TTL when expiresAt is past |
|
||||
|
||||
### `get(key): Promise<IdempotencyRecord<T> | undefined>`
|
||||
|
||||
Fetches and JSON-parses the record. Returns `undefined` if not found (key expired or never set).
|
||||
|
||||
### `set(key, record): Promise<void>`
|
||||
|
||||
Serialises the record as JSON and stores it with a TTL computed from `record.expiresAt - Date.now()`.
|
||||
|
||||
### `delete(key): Promise<boolean>`
|
||||
|
||||
Calls `DEL` on the prefixed key; returns `true` if a key was removed.
|
||||
|
||||
### `cleanup(beforeTimestamp): Promise<number>`
|
||||
|
||||
Scans all keys under the prefix, parses each, and deletes those with `expiresAt < beforeTimestamp`. Returns the count removed.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Redis TTL handles the common expiration case automatically; `cleanup` is only needed for records that were not expired by Redis (e.g. records with very long TTLs where early manual deletion is desired).
|
||||
- `scan` is used rather than `KEYS` to avoid blocking the Redis event loop in production.
|
||||
- JSON serialisation assumes records are fully serialisable; `Date` objects should be stored as timestamps (numbers).
|
||||
Reference in New Issue
Block a user