first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -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.