first commit
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
# `key-versioning`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides multi-version idempotency record management for scenarios where the same key may legitimately be reused with different input payloads over its lifetime. `KeyVersioning` wraps the idempotency store and enforces a configurable `ConflictResolutionStrategy`, maintaining an in-process version history for audit, rollback, and debugging.
|
||||
|
||||
## Features
|
||||
|
||||
- Three conflict strategies: `reject` (default), `overwrite`, `version`
|
||||
- In-process version history with configurable `maxVersions` cap
|
||||
- `resolve(key, input, fn?)` returns whether to execute and which version to write
|
||||
- `addVersionEntry` records a snapshot into the history
|
||||
- `getHistory`, `getLatestVersion`, `clearHistory` for inspection and cleanup
|
||||
- Generic `<T>` parameter for typed version entries
|
||||
- Compatible with any `IdempotencyStoreAdapter`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { KeyVersioning } from "@suaveplan/idempotency/versioning";
|
||||
import { MemoryIdempotencyStore } from "@suaveplan/idempotency/store";
|
||||
|
||||
const versioning = new KeyVersioning({
|
||||
store: new MemoryIdempotencyStore(),
|
||||
strategy: "version",
|
||||
maxVersions: 5,
|
||||
});
|
||||
|
||||
const { version, shouldExecute } = await versioning.resolve("order-42", payload);
|
||||
if (shouldExecute) {
|
||||
const result = await chargeCard(payload);
|
||||
versioning.addVersionEntry("order-42", {
|
||||
version,
|
||||
fingerprint: fingerprintPayload(payload),
|
||||
response: result,
|
||||
status: "completed",
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { KeyVersioning, VersionConflictError } from "@suaveplan/idempotency/versioning";
|
||||
|
||||
const versioning = new KeyVersioning({ store, strategy: "reject" });
|
||||
|
||||
try {
|
||||
const { shouldExecute } = await versioning.resolve("key", differentPayload);
|
||||
} catch (err) {
|
||||
if (err instanceof VersionConflictError) {
|
||||
// Log fingerprint conflict
|
||||
console.warn("Version conflict for key:", err.key);
|
||||
}
|
||||
}
|
||||
|
||||
// Inspect history
|
||||
const history = versioning.getHistory("order-42");
|
||||
console.log("Versions:", history.map((e) => e.version));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `new KeyVersioning<T>(config: KeyVersioningConfig)`
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `store` | `IdempotencyStoreAdapter` | required | Record persistence |
|
||||
| `strategy` | `ConflictResolutionStrategy` | `"reject"` | Conflict strategy |
|
||||
| `maxVersions` | `number` | `10` | Max history entries per key |
|
||||
|
||||
### `resolve(key, input, fingerprintFn?): Promise<{ version: number; shouldExecute: boolean }>`
|
||||
|
||||
Checks the store and applies the conflict strategy. Returns:
|
||||
|
||||
- `shouldExecute: true` — caller should invoke `fn`
|
||||
- `shouldExecute: false` — cached result can be returned
|
||||
|
||||
**Throws:** `VersionConflictError` when strategy is `'reject'` and a conflict is detected.
|
||||
|
||||
### `addVersionEntry(key, entry): void`
|
||||
|
||||
Appends a `VersionEntry<T>` to the in-process history. Trims to `maxVersions` if exceeded.
|
||||
|
||||
### `getHistory(key): VersionEntry<T>[]`
|
||||
|
||||
Returns all recorded version entries for the key, oldest first.
|
||||
|
||||
### `getLatestVersion(key): number`
|
||||
|
||||
Returns the latest version number, or `0` if no history exists.
|
||||
|
||||
### `clearHistory(key): void`
|
||||
|
||||
Removes all in-process history for the key.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Version history is in-process only; it is not persisted to the store. On process restart, history is empty even if the store contains records with version numbers.
|
||||
- `maxVersions` evicts the oldest entries from the left of the array, so `getLatestVersion` always reflects the most recent.
|
||||
- The `overwrite` strategy calls `store.delete(key)` before returning, ensuring the next `set` creates a fresh record at version `1`.
|
||||
- `VersionConflictError` carries both `existingFingerprint` and `newFingerprint` in its context for downstream diagnostics.
|
||||
Reference in New Issue
Block a user