@gravito/stasis 3.1.1 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Error thrown when a lock cannot be acquired within the specified timeout.
3
+ *
4
+ * This error indicates that the maximum waiting time for a distributed lock
5
+ * has been exceeded without successfully gaining ownership.
6
+ *
7
+ * @public
8
+ * @since 3.0.0
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * try {
13
+ * await cache.lock('resource', 10).block(5, async () => {
14
+ * // ...
15
+ * });
16
+ * } catch (error) {
17
+ * if (error instanceof LockTimeoutError) {
18
+ * console.error('Failed to acquire lock within 5 seconds');
19
+ * }
20
+ * }
21
+ * ```
22
+ */
23
+ export declare class LockTimeoutError extends Error {
24
+ name: string;
25
+ }
26
+ /**
27
+ * Interface for a cache-backed distributed lock.
28
+ *
29
+ * A distributed lock ensures mutual exclusion across multiple processes or
30
+ * instances by using a shared cache as the synchronization primitive.
31
+ *
32
+ * @public
33
+ * @since 3.0.0
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * const lock = cache.lock('process', 60);
38
+ * const result = await lock.block(10, async () => {
39
+ * // Exclusive work
40
+ * return 42;
41
+ * });
42
+ * ```
43
+ */
44
+ export interface CacheLock {
45
+ /**
46
+ * Attempt to acquire the lock immediately.
47
+ *
48
+ * Uses an atomic "set if not exists" operation in the underlying cache
49
+ * to ensure only one owner can hold the lock at a time.
50
+ *
51
+ * @returns `true` if the lock was successfully acquired, `false` if it is already held.
52
+ * @throws {Error} If the underlying cache store fails.
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * const acquired = await lock.acquire();
57
+ * if (acquired) {
58
+ * try {
59
+ * // Critical section
60
+ * } finally {
61
+ * await lock.release();
62
+ * }
63
+ * }
64
+ * ```
65
+ */
66
+ acquire(): Promise<boolean>;
67
+ /**
68
+ * Release the lock.
69
+ *
70
+ * Removes the lock entry from the cache, allowing other processes to acquire it.
71
+ * Should typically be called in a `finally` block to ensure the lock is not leaked.
72
+ *
73
+ * @returns A promise that resolves when the lock is released.
74
+ * @throws {Error} If the underlying cache store fails.
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * await lock.release();
79
+ * ```
80
+ */
81
+ release(): Promise<void>;
82
+ /**
83
+ * Extend the lock's time-to-live (TTL).
84
+ *
85
+ * Increases the expiration time of the lock to prevent it from being
86
+ * automatically released while a long-running task is still in progress.
87
+ *
88
+ * @param seconds - Duration in seconds to add to the current TTL.
89
+ * @returns `true` if the lock was extended (still owned by this process), `false` otherwise.
90
+ * @throws {Error} If the underlying cache store fails.
91
+ *
92
+ * @example
93
+ * ```typescript
94
+ * const extended = await lock.extend(30);
95
+ * if (!extended) {
96
+ * throw new Error('Lock lost or expired before extension');
97
+ * }
98
+ * ```
99
+ */
100
+ extend?(seconds: number): Promise<boolean>;
101
+ /**
102
+ * Get the remaining time-to-live for the lock.
103
+ *
104
+ * Useful for monitoring or determining if a lock extension is necessary.
105
+ *
106
+ * @returns Remaining TTL in seconds. Returns -1 if the lock doesn't exist, or -2 if it has no TTL.
107
+ * @throws {Error} If the underlying cache store fails.
108
+ *
109
+ * @example
110
+ * ```typescript
111
+ * const ttl = await lock.getRemainingTime();
112
+ * if (ttl > 0 && ttl < 5) {
113
+ * await lock.extend(10);
114
+ * }
115
+ * ```
116
+ */
117
+ getRemainingTime?(): Promise<number>;
118
+ /**
119
+ * Attempt to acquire the lock and execute a callback with automatic retry.
120
+ *
121
+ * If the lock is currently held, this method will poll the cache at regular
122
+ * intervals until the lock is acquired or the timeout is reached.
123
+ *
124
+ * @param seconds - Maximum duration to wait for the lock in seconds.
125
+ * @param callback - Logic to execute once the lock is successfully acquired.
126
+ * @param options - Configuration for polling and retry behavior.
127
+ * @returns The value returned by the callback.
128
+ * @throws {LockTimeoutError} If the lock cannot be acquired within the specified timeout.
129
+ * @throws {Error} If the callback throws or the cache store fails.
130
+ *
131
+ * @example
132
+ * ```typescript
133
+ * const result = await lock.block(10, async () => {
134
+ * return await performAtomicOperation();
135
+ * });
136
+ * ```
137
+ */
138
+ block<T>(seconds: number, callback: () => Promise<T> | T, options?: BlockOptions): Promise<T>;
139
+ }
140
+ /**
141
+ * Configuration for the `block()` method's retry logic.
142
+ *
143
+ * Defines how the lock acquisition should behave when the lock is already held,
144
+ * including polling intervals and cancellation support.
145
+ *
146
+ * @public
147
+ * @since 3.1.0
148
+ *
149
+ * @example
150
+ * ```typescript
151
+ * const options: BlockOptions = {
152
+ * retryInterval: 250,
153
+ * maxRetries: 10
154
+ * };
155
+ * ```
156
+ */
157
+ export interface BlockOptions {
158
+ /**
159
+ * Delay between consecutive acquisition attempts.
160
+ * @defaultValue 100
161
+ */
162
+ retryInterval?: number;
163
+ /**
164
+ * Maximum number of times to attempt acquisition before failing.
165
+ * @defaultValue Infinity
166
+ */
167
+ maxRetries?: number;
168
+ /**
169
+ * Signal to allow external cancellation of the waiting process.
170
+ *
171
+ * @example
172
+ * ```typescript
173
+ * const controller = new AbortController();
174
+ * setTimeout(() => controller.abort(), 2000);
175
+ * await lock.block(10, task, { signal: controller.signal });
176
+ * ```
177
+ */
178
+ signal?: AbortSignal;
179
+ /**
180
+ * @deprecated Use `retryInterval` instead.
181
+ */
182
+ sleepMillis?: number;
183
+ }
184
+ /**
185
+ * Pause execution for a specified duration.
186
+ *
187
+ * Internal utility used for implementing polling delays in lock acquisition.
188
+ *
189
+ * @param ms - Duration to pause in milliseconds.
190
+ * @returns A promise that resolves after the delay.
191
+ * @internal
192
+ */
193
+ export declare function sleep(ms: number): Promise<void>;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Contract for a mechanism that predicts future cache key accesses.
3
+ *
4
+ * Implementations observe sequential access patterns to predict which keys
5
+ * are likely to be requested next, enabling prefetching strategies.
6
+ *
7
+ * @public
8
+ * @since 3.2.0
9
+ */
10
+ export interface AccessPredictor {
11
+ /**
12
+ * Record a cache key access to learn temporal patterns.
13
+ *
14
+ * @param key - Cache key currently being accessed.
15
+ */
16
+ record(key: string): void;
17
+ /**
18
+ * Predict potential future keys based on learned access history.
19
+ *
20
+ * @param key - Current cache key acting as the trigger for prediction.
21
+ * @returns Array of predicted keys, ordered by likelihood.
22
+ */
23
+ predict(key: string): string[];
24
+ /**
25
+ * Reset all learned transition probabilities and state.
26
+ */
27
+ reset(): void;
28
+ }
29
+ /**
30
+ * A simple Markov Chain predictor (Order-1).
31
+ *
32
+ * Records transitions (A -> B) between sequential accesses and predicts
33
+ * B when A is next encountered. This is particularly effective for
34
+ * predictable resource loading sequences.
35
+ *
36
+ * @public
37
+ * @since 3.2.0
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * const predictor = new MarkovPredictor();
42
+ * predictor.record('user:1');
43
+ * predictor.record('user:1:profile');
44
+ * const next = predictor.predict('user:1'); // ['user:1:profile']
45
+ * ```
46
+ */
47
+ export declare class MarkovPredictor implements AccessPredictor {
48
+ private transitions;
49
+ private lastKey;
50
+ private readonly maxNodes;
51
+ private readonly maxEdgesPerNode;
52
+ /**
53
+ * Initialize a new MarkovPredictor.
54
+ *
55
+ * @param options - Limits for internal transition graph to manage memory.
56
+ */
57
+ constructor(options?: {
58
+ maxNodes?: number;
59
+ maxEdgesPerNode?: number;
60
+ });
61
+ record(key: string): void;
62
+ predict(key: string): string[];
63
+ reset(): void;
64
+ }
@@ -0,0 +1,200 @@
1
+ import type { CacheLock } from './locks';
2
+ import type { CacheKey, CacheTtl, CacheValue } from './types';
3
+ /**
4
+ * Low-level cache storage contract.
5
+ *
6
+ * Defines the essential operations for cache backends. Implementations of this interface
7
+ * (e.g., Memory, Redis, File) provide the actual persistence logic for the cache system.
8
+ *
9
+ * @public
10
+ * @since 3.0.0
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * class MyStore implements CacheStore {
15
+ * async get(key) { ... }
16
+ * async put(key, value, ttl) { ... }
17
+ * // ... other methods
18
+ * }
19
+ * ```
20
+ */
21
+ export interface CacheStore {
22
+ /**
23
+ * Retrieve an item from the cache.
24
+ *
25
+ * Fetches the value associated with the given key. If the item has expired or does not exist,
26
+ * the implementation should return null.
27
+ *
28
+ * @param key - Unique identifier for the cached item.
29
+ * @returns The cached value or null if missing/expired.
30
+ * @throws {Error} If the storage backend is unreachable or encounters a read failure.
31
+ */
32
+ get<T = unknown>(key: CacheKey): Promise<CacheValue<T>>;
33
+ /**
34
+ * Store an item in the cache.
35
+ *
36
+ * Persists a value with a specific expiration time. Overwrites any existing value for the same key.
37
+ *
38
+ * @param key - Unique identifier for the cached item.
39
+ * @param value - Data to be persisted.
40
+ * @param ttl - Duration in seconds until the item expires.
41
+ * @returns Resolves when the write operation completes.
42
+ * @throws {Error} If the storage backend is full or encounters a write failure.
43
+ */
44
+ put(key: CacheKey, value: unknown, ttl: CacheTtl): Promise<void>;
45
+ /**
46
+ * Store an item if it does not already exist.
47
+ *
48
+ * Atomic operation to ensure a value is only stored if the key is currently vacant.
49
+ *
50
+ * @param key - Unique identifier for the cached item.
51
+ * @param value - Data to be persisted.
52
+ * @param ttl - Duration in seconds until the item expires.
53
+ * @returns True if the item was successfully added, false if it already existed.
54
+ * @throws {Error} If the storage backend encounters a concurrency or write failure.
55
+ */
56
+ add(key: CacheKey, value: unknown, ttl: CacheTtl): Promise<boolean>;
57
+ /**
58
+ * Remove an item from the cache.
59
+ *
60
+ * Deletes the entry associated with the specified key.
61
+ *
62
+ * @param key - Identifier of the item to be removed.
63
+ * @returns True if the item existed and was removed, false otherwise.
64
+ * @throws {Error} If the storage backend encounters a deletion failure.
65
+ */
66
+ forget(key: CacheKey): Promise<boolean>;
67
+ /**
68
+ * Wipe all items from the cache storage.
69
+ *
70
+ * Clears the entire cache backend. Use with caution as this operation is destructive.
71
+ *
72
+ * @returns Resolves when the flush operation completes.
73
+ * @throws {Error} If the storage backend fails to clear the data.
74
+ */
75
+ flush(): Promise<void>;
76
+ /**
77
+ * Increment a numeric value in the cache.
78
+ *
79
+ * Atomically increases the value of a numeric item. If the key does not exist,
80
+ * it is typically initialized to zero before incrementing.
81
+ *
82
+ * @param key - Identifier of the numeric item.
83
+ * @param value - Amount to add to the current value.
84
+ * @returns The updated numeric value.
85
+ * @throws {TypeError} If the existing value is not numeric.
86
+ * @throws {Error} If the storage backend encounters an atomic update failure.
87
+ */
88
+ increment(key: CacheKey, value?: number): Promise<number>;
89
+ /**
90
+ * Decrement a numeric value in the cache.
91
+ *
92
+ * Atomically decreases the value of a numeric item. If the key does not exist,
93
+ * it is typically initialized to zero before decrementing.
94
+ *
95
+ * @param key - Identifier of the numeric item.
96
+ * @param value - Amount to subtract from the current value.
97
+ * @returns The updated numeric value.
98
+ * @throws {TypeError} If the existing value is not numeric.
99
+ * @throws {Error} If the storage backend encounters an atomic update failure.
100
+ */
101
+ decrement(key: CacheKey, value?: number): Promise<number>;
102
+ /**
103
+ * Create a distributed lock instance.
104
+ *
105
+ * Provides a mechanism for mutual exclusion across multiple processes or servers
106
+ * using the cache backend as the synchronization provider.
107
+ *
108
+ * @param name - Unique name for the lock.
109
+ * @param seconds - Default duration for which the lock should be held.
110
+ * @returns A lock instance if supported by the driver, otherwise undefined.
111
+ */
112
+ lock?(name: string, seconds?: number): CacheLock | undefined;
113
+ /**
114
+ * Get the remaining lifetime of a cached item.
115
+ *
116
+ * Calculates how many seconds are left before the item expires.
117
+ *
118
+ * @param key - Identifier of the cached item.
119
+ * @returns Seconds remaining until expiration, or null if the key has no TTL or does not exist.
120
+ * @throws {Error} If the storage backend encounters a read failure.
121
+ */
122
+ ttl?(key: CacheKey): Promise<number | null>;
123
+ }
124
+ /**
125
+ * Contract for cache stores supporting tag-based invalidation.
126
+ *
127
+ * Allows grouping cache entries under one or more tags, enabling bulk invalidation
128
+ * of related items without knowing their individual keys.
129
+ *
130
+ * @public
131
+ * @since 3.0.0
132
+ *
133
+ * @example
134
+ * ```typescript
135
+ * if (isTaggableStore(store)) {
136
+ * await store.flushTags(['users']);
137
+ * }
138
+ * ```
139
+ */
140
+ export interface TaggableStore {
141
+ /**
142
+ * Invalidate all items associated with specific tags.
143
+ *
144
+ * Effectively clears all cache entries that were stored with any of the provided tags.
145
+ *
146
+ * @param tags - List of tags to be flushed.
147
+ * @returns Resolves when the tag invalidation completes.
148
+ * @throws {Error} If the storage backend fails to process the tag flush.
149
+ */
150
+ flushTags(tags: readonly string[]): Promise<void>;
151
+ /**
152
+ * Compute a namespaced key based on tags.
153
+ *
154
+ * Generates a unique internal key that incorporates tag versioning to ensure
155
+ * proper isolation and invalidation.
156
+ *
157
+ * @param key - Original user-provided cache key.
158
+ * @param tags - Tags to associate with the key.
159
+ * @returns A derived key string used for actual storage.
160
+ */
161
+ tagKey(key: string, tags: readonly string[]): string;
162
+ /**
163
+ * Register a key in the tag index.
164
+ *
165
+ * Maintains the relationship between tags and their associated keys for tracking.
166
+ *
167
+ * @param tags - Tags to index the key under.
168
+ * @param taggedKey - The derived key to be indexed.
169
+ * @returns Resolves when the indexing completes.
170
+ */
171
+ tagIndexAdd(tags: readonly string[], taggedKey: string): void | Promise<void>;
172
+ /**
173
+ * Unregister a key from all tag indexes.
174
+ *
175
+ * Removes the tracking information for a specific derived key.
176
+ *
177
+ * @param taggedKey - The derived key to remove from indexes.
178
+ * @returns Resolves when the removal completes.
179
+ */
180
+ tagIndexRemove(taggedKey: string): void | Promise<void>;
181
+ }
182
+ /**
183
+ * Validates if a cache store supports tagging operations.
184
+ *
185
+ * Performs a runtime check to determine if the provided store implements the `TaggableStore` interface.
186
+ *
187
+ * @param store - The cache store instance to evaluate.
188
+ * @returns True if the store supports tagging, false otherwise.
189
+ *
190
+ * @example
191
+ * ```typescript
192
+ * if (isTaggableStore(myStore)) {
193
+ * await myStore.flushTags(['users', 'posts']);
194
+ * }
195
+ * ```
196
+ *
197
+ * @public
198
+ * @since 3.0.0
199
+ */
200
+ export declare function isTaggableStore(store: CacheStore): store is CacheStore & TaggableStore;
@@ -0,0 +1,78 @@
1
+ import type { CacheStore } from '../store';
2
+ import type { CacheKey, CacheTtl } from '../types';
3
+ export type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
4
+ /**
5
+ * Options for the CircuitBreakerStore.
6
+ *
7
+ * @public
8
+ * @since 3.2.0
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * const options: CircuitBreakerOptions = {
13
+ * maxFailures: 3,
14
+ * resetTimeout: 30000,
15
+ * fallback: new MemoryStore()
16
+ * };
17
+ * ```
18
+ */
19
+ export type CircuitBreakerOptions = {
20
+ /**
21
+ * Number of consecutive failures before opening the circuit.
22
+ * @defaultValue 5
23
+ */
24
+ maxFailures?: number;
25
+ /**
26
+ * Time in milliseconds to stay in OPEN state before transitioning to HALF_OPEN.
27
+ * @defaultValue 60000
28
+ */
29
+ resetTimeout?: number;
30
+ /**
31
+ * Optional fallback store to use when the primary store is unavailable.
32
+ */
33
+ fallback?: CacheStore;
34
+ };
35
+ /**
36
+ * A protective wrapper for cache stores that prevents cascading failures
37
+ * through circuit-breaking logic.
38
+ *
39
+ * When the primary store encounters repeated failures, the circuit opens,
40
+ * temporarily bypassing the primary store and optionally using a fallback.
41
+ *
42
+ * @public
43
+ * @since 3.2.0
44
+ */
45
+ export declare class CircuitBreakerStore implements CacheStore {
46
+ private readonly primary;
47
+ private state;
48
+ private failures;
49
+ private lastErrorTime;
50
+ private options;
51
+ constructor(primary: CacheStore, options?: CircuitBreakerOptions);
52
+ private execute;
53
+ private onSuccess;
54
+ private onFailure;
55
+ private handleFallback;
56
+ get<T = unknown>(key: CacheKey): Promise<T | null>;
57
+ put(key: CacheKey, value: unknown, ttl: CacheTtl): Promise<void>;
58
+ add(key: CacheKey, value: unknown, ttl: CacheTtl): Promise<boolean>;
59
+ forget(key: CacheKey): Promise<boolean>;
60
+ flush(): Promise<void>;
61
+ increment(key: CacheKey, value?: number): Promise<number>;
62
+ decrement(key: CacheKey, value?: number): Promise<number>;
63
+ ttl(key: CacheKey): Promise<number | null>;
64
+ /**
65
+ * Returns current state for monitoring.
66
+ *
67
+ * @returns Current state of the circuit breaker.
68
+ *
69
+ * @example
70
+ * ```typescript
71
+ * const state = store.getState();
72
+ * if (state === 'OPEN') {
73
+ * console.warn('Primary cache is unavailable');
74
+ * }
75
+ * ```
76
+ */
77
+ getState(): CircuitState;
78
+ }
@@ -0,0 +1,36 @@
1
+ import { type CacheLock } from '../locks';
2
+ import type { CacheStore } from '../store';
3
+ import { type CacheKey, type CacheTtl, type CacheValue } from '../types';
4
+ /**
5
+ * Configuration options for the `FileStore` implementation.
6
+ */
7
+ export type FileStoreOptions = {
8
+ directory: string;
9
+ enableCleanup?: boolean;
10
+ cleanupInterval?: number;
11
+ maxFiles?: number;
12
+ useSubdirectories?: boolean;
13
+ };
14
+ /**
15
+ * A persistent filesystem-based implementation of the `CacheStore` interface.
16
+ */
17
+ export declare class FileStore implements CacheStore {
18
+ private options;
19
+ private cleanupTimer;
20
+ private runtime;
21
+ constructor(options: FileStoreOptions);
22
+ private startCleanupDaemon;
23
+ cleanExpiredFiles(): Promise<number>;
24
+ destroy(): Promise<void>;
25
+ private ensureDir;
26
+ private filePathForKey;
27
+ get<T = unknown>(key: CacheKey): Promise<CacheValue<T>>;
28
+ put(key: CacheKey, value: unknown, ttl: CacheTtl): Promise<void>;
29
+ add(key: CacheKey, value: unknown, ttl: CacheTtl): Promise<boolean>;
30
+ forget(key: CacheKey): Promise<boolean>;
31
+ flush(): Promise<void>;
32
+ increment(key: CacheKey, value?: number): Promise<number>;
33
+ decrement(key: CacheKey, value?: number): Promise<number>;
34
+ ttl(key: CacheKey): Promise<number | null>;
35
+ lock(name: string, seconds?: number): CacheLock;
36
+ }