@siglo/redis 0.0.0-stage → 1.0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 Jerico de guzman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,227 @@
1
- # Temporary Holding Version
1
+ # @siglo/redis Adapter Guide
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ `@siglo/redis` provides an in-memory distributed persistence, step response caching, atomic idempotency deduplication, and distributed single-flight coordination layer for `Siglo` workflows. It is powered by Redis and compatible with standalone Redis, Redis Sentinel, and Redis Cluster via `ioredis`.
4
+
5
+ ---
6
+
7
+ ## 1. Overview & Architecture
8
+
9
+ The Redis adapter family encompasses three core modules:
10
+
11
+ ```mermaid
12
+ flowchart TD
13
+ subgraph Package["@siglo/redis"]
14
+ direction LR
15
+ SA["RedisStorageAdapter<br/>• Ephemeral State<br/>• Step Caching<br/>• Context Hash Stream"]
16
+ IS["RedisIdempotencyStore<br/>• Atomic NX Claims<br/>• Result Caching<br/>• Fast Lock Release"]
17
+ LK["RedisLock<br/>• Distributed Mutex<br/>• Token Verification<br/>• Safe Lua Releasing"]
18
+ end
19
+
20
+ subgraph RedisServer["Redis Server"]
21
+ Keys1["siglo:[ns:]checkpoint:id<br/>siglo:cache:key"]
22
+ Keys2["siglo:idemp:key"]
23
+ Keys3["siglo:lock:key<br/>siglo:[ns:]context:id:step"]
24
+ end
25
+
26
+ SA --> Keys1
27
+ SA --> Keys3
28
+ IS --> Keys2
29
+ LK --> Keys3
30
+ ```
31
+
32
+ ---
33
+
34
+ ## 2. Installation & Requirements
35
+
36
+ Install `@siglo/redis` alongside `ioredis`:
37
+
38
+ ```bash
39
+ npm install @siglo/core @siglo/redis ioredis
40
+ ```
41
+
42
+ Peer dependency matrix:
43
+ - `Siglo`: `*` (v1.0.0+)
44
+ - `ioredis`: `^5.0.0 || ^6.0.0`
45
+
46
+ ---
47
+
48
+ ## 3. Key Topology & Redis Data Structures
49
+
50
+ `@siglo/redis` uses structured key namespaces:
51
+
52
+ | Key Pattern | Redis Type | Default TTL | Description |
53
+ | :--- | :--- | :--- | :--- |
54
+ | `siglo:[<ns>:]checkpoint:<stackId>` | `string` (JSON) | 7 days (`604800000ms`) | Serialized execution stack for workflow resumption. Partitioned by tenant namespace if provided (`siglo:<ns>:checkpoint:<stackId>`). |
55
+ | `siglo:cache:<key>` | `string` (JSON) | 24 hours (`86400000ms`) | Cached step evaluation output. |
56
+ | `siglo:[<ns>:]context:<stackId>:<stepId>` | `hash` | 7 days (`604800000ms`) | Active context field mutations for auditing. Partitioned by tenant namespace if provided (`siglo:<ns>:context:<stackId>:<stepId>`). |
57
+ | `siglo:idemp:<key>` | `string` | 24 hours (`86400000ms`) | Idempotency lock status (`in-progress` or `COMPLETED:<json>`). |
58
+ | `siglo:lock:<key>` | `string` (UUID) | Caller specified (e.g. 10s) | Distributed mutex lock holding worker token. |
59
+
60
+ ---
61
+
62
+ ## 4. `RedisStorageAdapter` Reference
63
+
64
+ Implements the standard `StorageAdapter` interface for high-performance checkpointing and step caching.
65
+
66
+ ```typescript
67
+ import { RedisStorageAdapter } from '@siglo/redis';
68
+ ```
69
+
70
+ ### Options (`RedisStorageAdapterOptions`)
71
+
72
+ | Option | Type | Default | Description |
73
+ | :--- | :--- | :--- | :--- |
74
+ | `client` | `RedisClientInterface \| Redis` | *Required* | Active `ioredis` instance. |
75
+ | `keyPrefix` | `string` | `'siglo:'` | Root prefix for all Redis keys. |
76
+ | `checkpointTtlMs` | `number` | `604800000` (7 days) | Checkpoint expiration TTL. |
77
+ | `cacheTtlMs` | `number` | `86400000` (24 hours) | Default step response cache expiration TTL. |
78
+ | `recordContextChanges` | `boolean` | `true` | When `true`, streams field updates to Redis Hashes. |
79
+
80
+ ### Methods
81
+
82
+ - **`saveCheckpoint(checkpoint)`**: Saves JSON-serialized execution stack with millisecond TTL (`SET key val PX ttl`).
83
+ - **`getCheckpoint(stackId, namespace?)`**: Retrieves and parses serialized checkpoint (checks namespaced key first, falling back to legacy key). Returns `undefined` if not found.
84
+ - **`getCachedStep(key)`**: Retrieves cached step response.
85
+ - **`setCachedStep(key, value, ttlMs?)`**: Writes cached step response with specified TTL.
86
+ - **`onContextChange(entry)`**: Executes `HSET` on `siglo:[<ns>:]context:<stackId>:<stepId>` and refreshes TTL with `PEXPIRE`.
87
+
88
+ ---
89
+
90
+ ## 5. `RedisIdempotencyStore` Reference
91
+
92
+ Provides distributed, atomic idempotency guarantees across multi-instance clusters using Redis `SET key val PX ttl NX` semantics.
93
+
94
+ ```typescript
95
+ import { RedisIdempotencyStore } from '@siglo/redis';
96
+ ```
97
+
98
+ ### Options (`RedisIdempotencyOptions`)
99
+
100
+ | Option | Type | Default | Description |
101
+ | :--- | :--- | :--- | :--- |
102
+ | `client` | `RedisClientInterface \| Redis` | *Required* | Active `ioredis` instance. |
103
+ | `keyPrefix` | `string` | `'siglo:idemp:'` | Prefix for idempotency keys. |
104
+ | `defaultTtlMs` | `number` | `86400000` (24 hours) | Default lock TTL. |
105
+
106
+ ### Claim States & Transitions:
107
+ 1. **`claim(key, ttlMs?)`**:
108
+ - Executes atomic Redis `SET <key> "in-progress" PX <ttl> NX`.
109
+ - If Redis returns `'OK'`: Returns `'claimed'`.
110
+ - If key exists and starts with `COMPLETED:`: Returns `'completed'`.
111
+ - If key exists with value `in-progress`: Returns `'in-progress'`.
112
+ 2. **`complete(key, result, ttlMs?)`**:
113
+ - Overwrites key with `COMPLETED:${JSON.stringify(result)}`.
114
+ 3. **`fail(key, error)`**:
115
+ - Deletes the key if it is currently `'in-progress'`, freeing the lock so subsequent retries can proceed.
116
+ 4. **`getResult(key)`**:
117
+ - Returns the deserialized result for completed keys.
118
+
119
+ ---
120
+
121
+ ## 6. `RedisLock` (Distributed Mutex & Single-Flight Coordination)
122
+
123
+ `RedisLock` prevents duplicate parallel execution of sensitive workflows (e.g., end-of-month payroll, nightly data synchronization) across multiple server instances.
124
+
125
+ ```typescript
126
+ import { RedisLock, RedisLockOptions } from '@siglo/redis';
127
+
128
+ // Simple prefix configuration:
129
+ const lock = new RedisLock(redis, 'siglo:lock:');
130
+
131
+ // Full options configuration:
132
+ const strictLock = new RedisLock(redis, {
133
+ keyPrefix: 'siglo:lock:',
134
+ strictAtomicity: true, // Enforces Redis eval (Lua) for atomic lock release
135
+ });
136
+ ```
137
+
138
+ ### Options (`RedisLockOptions`)
139
+
140
+ | Option | Type | Default | Description |
141
+ | :--- | :--- | :--- | :--- |
142
+ | `keyPrefix` | `string` | `'siglo:lock:'` | Root prefix for distributed mutex keys. |
143
+ | `strictAtomicity` | `boolean` | `false` | When `true`, enforces strict atomic Lua evaluation (`client.eval`) during lock release, throwing an error if Lua scripting is unavailable rather than falling back to non-atomic commands, preventing TOCTOU race conditions. |
144
+
145
+ ### Methods:
146
+
147
+ #### `acquire(lockKey, ttlMs?, customToken?)`
148
+ Attempts atomic acquisition (`SET key token PX ttl NX`). Returns `{ acquired: boolean, token: string }`.
149
+
150
+ #### `release(lockKey, token)`
151
+ Safely releases the lock only if the current value matches the caller's unique token. Uses atomic Lua scripting:
152
+ ```lua
153
+ if redis.call("get", KEYS[1]) == ARGV[1] then
154
+ return redis.call("del", KEYS[1])
155
+ else
156
+ return 0
157
+ end
158
+ ```
159
+ When `strictAtomicity` is enabled, failure or absence of Lua `eval` will throw an error to prevent non-atomic release races.
160
+
161
+ #### `withLock(lockKey, fn, ttlMs?)`
162
+ Executes an asynchronous function wrapped in single-flight protection:
163
+ - Acquires lock.
164
+ - If acquisition fails, throws `Error('Failed to acquire lock for key: ...')`.
165
+ - Executes `fn()`.
166
+ - Releases lock in a `finally` block, ensuring no leaked locks even on unhandled exceptions.
167
+
168
+ ---
169
+
170
+ ## 7. Complete Production Example
171
+
172
+ ```typescript
173
+ import { Redis } from 'ioredis';
174
+ import { Step, flow } from '@siglo/core';
175
+ import {
176
+ RedisStorageAdapter,
177
+ RedisIdempotencyStore,
178
+ RedisLock
179
+ } from '@siglo/redis';
180
+
181
+ // 1. Initialize Redis Client
182
+ const redis = new Redis('redis://localhost:6379');
183
+
184
+ // 2. Register Global Storage
185
+ const storageAdapter = new RedisStorageAdapter({
186
+ client: redis,
187
+ checkpointTtlMs: 86400000, // 24 hours
188
+ });
189
+ Step.useStorage(storageAdapter);
190
+
191
+ // 3. Initialize Idempotency & Distributed Lock
192
+ const idempotencyStore = new RedisIdempotencyStore({ client: redis });
193
+ const redisLock = new RedisLock(redis, { strictAtomicity: true });
194
+
195
+ // 4. Define Workflow with Step Caching
196
+ const fetchExchangeRates = Step.create('fetchRates', async () => {
197
+ // Heavy external HTTP call
198
+ return { EUR: 1.08, GBP: 1.27, JPY: 154.2 };
199
+ })
200
+ .cache({ ttlMs: 300000, key: () => 'currency-rates-latest' }) // 5-minute cache
201
+ ;
202
+
203
+ const settleInvoice = Step.create('settleInvoice', async ({ invoiceId, amount, currency }: any) => {
204
+ return { invoiceId, settled: true, amountUSD: amount * 1.08 };
205
+ });
206
+
207
+ const billingFlow = flow(fetchExchangeRates, settleInvoice);
208
+
209
+ // 5. Single-Flight Execution Runner
210
+ async function processDailySettlement() {
211
+ // Only one cluster node can execute this block at a time
212
+ return await redisLock.withLock('lock:daily-settlement', async () => {
213
+ console.log('Acquired single-flight lock. Starting settlement flow...');
214
+ return await billingFlow.run({
215
+ invoiceId: 'INV-2026-10',
216
+ amount: 5000,
217
+ currency: 'EUR',
218
+ });
219
+ }, 15000); // 15-second lock timeout
220
+ }
221
+
222
+ // 6. Graceful Shutdown
223
+ process.on('SIGTERM', async () => {
224
+ await redis.quit();
225
+ process.exit(0);
226
+ });
227
+ ```
@@ -0,0 +1,31 @@
1
+ import { StorageAdapter, SerializedExecutionStack } from '@siglo/core';
2
+ import { RedisStorageAdapterOptions } from './types';
3
+ export declare class RedisStorageAdapter implements StorageAdapter {
4
+ readonly name = "redis";
5
+ private client;
6
+ private keyPrefix;
7
+ private cacheTtlMs;
8
+ private checkpointTtlMs;
9
+ private recordContextChanges;
10
+ constructor(options: RedisStorageAdapterOptions);
11
+ private cacheKey;
12
+ private checkpointKey;
13
+ private waitRecordKey;
14
+ private contextKey;
15
+ onContextChange(entry: {
16
+ namespace?: string;
17
+ stackId: string;
18
+ stepId: string;
19
+ field: string;
20
+ value: any;
21
+ timestamp: number;
22
+ }): Promise<void>;
23
+ saveCheckpoint(checkpoint: SerializedExecutionStack): Promise<void>;
24
+ getCheckpoint(stackId: string, namespace?: string): Promise<SerializedExecutionStack | undefined>;
25
+ cleanExpiredCheckpoints(): Promise<number>;
26
+ saveWaitRecord(record: any): Promise<void>;
27
+ getWaitRecord(correlationKeyOrStackId: string): Promise<any | undefined>;
28
+ getCachedStep(key: string): Promise<any | undefined>;
29
+ setCachedStep(key: string, value: any, ttlMs?: number): Promise<void>;
30
+ }
31
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,EAAE,0BAA0B,EAAwB,MAAM,SAAS,CAAC;AAE3E,qBAAa,mBAAoB,YAAW,cAAc;IACtD,SAAgB,IAAI,WAAW;IAC/B,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,oBAAoB,CAAU;gBAE1B,OAAO,EAAE,0BAA0B;IAW/C,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,aAAa;IAMrB,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,UAAU;IAMZ,eAAe,CAAC,KAAK,EAAE;QACzB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,EAAE,GAAG,CAAC;QACX,SAAS,EAAE,MAAM,CAAC;KACrB,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBX,cAAc,CAAC,UAAU,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IASnE,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,wBAAwB,GAAG,SAAS,CAAC;IAqBjG,uBAAuB,IAAI,OAAO,CAAC,MAAM,CAAC;IAI1C,cAAc,CAAC,MAAM,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAa1C,aAAa,CAAC,uBAAuB,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,GAAG,SAAS,CAAC;IAgBxE,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,GAAG,SAAS,CAAC;IAgBpD,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAS9E"}
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RedisStorageAdapter = void 0;
4
+ class RedisStorageAdapter {
5
+ name = 'redis';
6
+ client;
7
+ keyPrefix;
8
+ cacheTtlMs;
9
+ checkpointTtlMs;
10
+ recordContextChanges;
11
+ constructor(options) {
12
+ if (!options || !options.client) {
13
+ throw new Error('RedisStorageAdapter requires a valid Redis client instance.');
14
+ }
15
+ this.client = options.client;
16
+ this.keyPrefix = options.keyPrefix || 'siglo:';
17
+ this.cacheTtlMs = options.cacheTtlMs || 86400000; // 24 hours
18
+ this.checkpointTtlMs = options.checkpointTtlMs || 604800000; // 7 days
19
+ this.recordContextChanges = options.recordContextChanges ?? true;
20
+ }
21
+ cacheKey(key) {
22
+ return `${this.keyPrefix}cache:${key}`;
23
+ }
24
+ checkpointKey(stackId, namespace) {
25
+ return namespace
26
+ ? `${this.keyPrefix}${namespace}:checkpoint:${stackId}`
27
+ : `${this.keyPrefix}checkpoint:${stackId}`;
28
+ }
29
+ waitRecordKey(key) {
30
+ return `${this.keyPrefix}wait:${key}`;
31
+ }
32
+ contextKey(stackId, stepId, namespace) {
33
+ return namespace
34
+ ? `${this.keyPrefix}${namespace}:context:${stackId}:${stepId}`
35
+ : `${this.keyPrefix}context:${stackId}:${stepId}`;
36
+ }
37
+ async onContextChange(entry) {
38
+ if (!this.recordContextChanges) {
39
+ return;
40
+ }
41
+ try {
42
+ const key = this.contextKey(entry.stackId, entry.stepId, entry.namespace);
43
+ const serialized = typeof entry.value === 'object' && entry.value !== null
44
+ ? JSON.stringify(entry.value)
45
+ : String(entry.value ?? '');
46
+ await this.client.hset(key, entry.field, serialized);
47
+ if (this.client.pexpire) {
48
+ await this.client.pexpire(key, this.checkpointTtlMs);
49
+ }
50
+ }
51
+ catch {
52
+ // Non-blocking telemetry error: silently ignore to protect execution flow
53
+ }
54
+ }
55
+ async saveCheckpoint(checkpoint) {
56
+ if (!checkpoint || !checkpoint.stackId) {
57
+ return;
58
+ }
59
+ const key = this.checkpointKey(checkpoint.stackId, checkpoint.namespace);
60
+ const serialized = JSON.stringify(checkpoint);
61
+ await this.client.set(key, serialized, 'PX', this.checkpointTtlMs);
62
+ }
63
+ async getCheckpoint(stackId, namespace) {
64
+ if (!stackId) {
65
+ return undefined;
66
+ }
67
+ let data = null;
68
+ if (namespace) {
69
+ data = await this.client.get(this.checkpointKey(stackId, namespace));
70
+ }
71
+ if (!data) {
72
+ data = await this.client.get(this.checkpointKey(stackId));
73
+ }
74
+ if (!data) {
75
+ return undefined;
76
+ }
77
+ try {
78
+ return JSON.parse(data);
79
+ }
80
+ catch {
81
+ return undefined;
82
+ }
83
+ }
84
+ async cleanExpiredCheckpoints() {
85
+ return 0; // Handled automatically by Redis TTL expiration
86
+ }
87
+ async saveWaitRecord(record) {
88
+ if (!record || (!record.correlationKey && !record.stackId)) {
89
+ return;
90
+ }
91
+ const serialized = JSON.stringify(record);
92
+ if (record.correlationKey) {
93
+ await this.client.set(this.waitRecordKey(record.correlationKey), serialized, 'PX', this.checkpointTtlMs);
94
+ }
95
+ if (record.stackId) {
96
+ await this.client.set(this.waitRecordKey(record.stackId), serialized, 'PX', this.checkpointTtlMs);
97
+ }
98
+ }
99
+ async getWaitRecord(correlationKeyOrStackId) {
100
+ if (!correlationKeyOrStackId) {
101
+ return undefined;
102
+ }
103
+ const data = await this.client.get(this.waitRecordKey(correlationKeyOrStackId));
104
+ if (data) {
105
+ try {
106
+ return JSON.parse(data);
107
+ }
108
+ catch {
109
+ return undefined;
110
+ }
111
+ }
112
+ const cp = await this.getCheckpoint(correlationKeyOrStackId);
113
+ return cp?.metadata?.waitRecord;
114
+ }
115
+ async getCachedStep(key) {
116
+ if (!key) {
117
+ return undefined;
118
+ }
119
+ const fullKey = this.cacheKey(key);
120
+ const data = await this.client.get(fullKey);
121
+ if (!data) {
122
+ return undefined;
123
+ }
124
+ try {
125
+ return JSON.parse(data);
126
+ }
127
+ catch {
128
+ return data;
129
+ }
130
+ }
131
+ async setCachedStep(key, value, ttlMs) {
132
+ if (!key) {
133
+ return;
134
+ }
135
+ const fullKey = this.cacheKey(key);
136
+ const effectiveTtl = ttlMs || this.cacheTtlMs;
137
+ const serialized = typeof value === 'object' && value !== null ? JSON.stringify(value) : JSON.stringify({ value });
138
+ await this.client.set(fullKey, serialized, 'PX', effectiveTtl);
139
+ }
140
+ }
141
+ exports.RedisStorageAdapter = RedisStorageAdapter;
142
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import { IdempotencyStore } from '@siglo/core';
2
+ import { RedisIdempotencyOptions } from './types';
3
+ export declare class RedisIdempotencyStore implements IdempotencyStore {
4
+ private client;
5
+ private keyPrefix;
6
+ private defaultTtlMs;
7
+ constructor(options: RedisIdempotencyOptions);
8
+ private formatKey;
9
+ claim(key: string, ttlMs?: number): Promise<'claimed' | 'completed' | 'in-progress'>;
10
+ complete(key: string, result: unknown, ttlMs?: number): Promise<void>;
11
+ fail(key: string, _error: unknown): Promise<void>;
12
+ getResult(key: string): Promise<unknown | undefined>;
13
+ }
14
+ //# sourceMappingURL=idempotency.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idempotency.d.ts","sourceRoot":"","sources":["../src/idempotency.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,uBAAuB,EAAwB,MAAM,SAAS,CAAC;AAKxE,qBAAa,qBAAsB,YAAW,gBAAgB;IAC1D,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,YAAY,CAAS;gBAEjB,OAAO,EAAE,uBAAuB;IAS5C,OAAO,CAAC,SAAS;IAIX,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,WAAW,GAAG,aAAa,CAAC;IAmBpF,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOrE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IASjD,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC;CAY7D"}
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RedisIdempotencyStore = void 0;
4
+ const COMPLETED_PREFIX = 'COMPLETED:';
5
+ const IN_PROGRESS_VAL = 'in-progress';
6
+ class RedisIdempotencyStore {
7
+ client;
8
+ keyPrefix;
9
+ defaultTtlMs;
10
+ constructor(options) {
11
+ if (!options || !options.client) {
12
+ throw new Error('RedisIdempotencyStore requires a valid Redis client instance.');
13
+ }
14
+ this.client = options.client;
15
+ this.keyPrefix = options.keyPrefix || 'siglo:idemp:';
16
+ this.defaultTtlMs = options.defaultTtlMs || 86400000; // 24 hours default
17
+ }
18
+ formatKey(key) {
19
+ return `${this.keyPrefix}${key}`;
20
+ }
21
+ async claim(key, ttlMs) {
22
+ const fullKey = this.formatKey(key);
23
+ const effectiveTtl = ttlMs || this.defaultTtlMs;
24
+ // Atomic lock attempt: SET fullKey "in-progress" NX PX effectiveTtl
25
+ const response = await this.client.set(fullKey, IN_PROGRESS_VAL, 'PX', effectiveTtl, 'NX');
26
+ if (response === 'OK') {
27
+ return 'claimed';
28
+ }
29
+ // Already exists: inspect current state
30
+ const existing = await this.client.get(fullKey);
31
+ if (existing && existing.startsWith(COMPLETED_PREFIX)) {
32
+ return 'completed';
33
+ }
34
+ return 'in-progress';
35
+ }
36
+ async complete(key, result, ttlMs) {
37
+ const fullKey = this.formatKey(key);
38
+ const effectiveTtl = ttlMs || this.defaultTtlMs;
39
+ const payload = COMPLETED_PREFIX + JSON.stringify(result ?? null);
40
+ await this.client.set(fullKey, payload, 'PX', effectiveTtl);
41
+ }
42
+ async fail(key, _error) {
43
+ const fullKey = this.formatKey(key);
44
+ // Clear in-flight claim to allow subsequent retry execution
45
+ const existing = await this.client.get(fullKey);
46
+ if (existing === IN_PROGRESS_VAL) {
47
+ await this.client.del(fullKey);
48
+ }
49
+ }
50
+ async getResult(key) {
51
+ const fullKey = this.formatKey(key);
52
+ const data = await this.client.get(fullKey);
53
+ if (data && data.startsWith(COMPLETED_PREFIX)) {
54
+ try {
55
+ return JSON.parse(data.slice(COMPLETED_PREFIX.length));
56
+ }
57
+ catch {
58
+ return undefined;
59
+ }
60
+ }
61
+ return undefined;
62
+ }
63
+ }
64
+ exports.RedisIdempotencyStore = RedisIdempotencyStore;
65
+ //# sourceMappingURL=idempotency.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idempotency.js","sourceRoot":"","sources":["../src/idempotency.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export * from './types';
2
+ export * from './adapter';
3
+ export * from './idempotency';
4
+ export * from './lock';
5
+ export * from './nonce';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,QAAQ,CAAC;AACvB,cAAc,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./types"), exports);
18
+ __exportStar(require("./adapter"), exports);
19
+ __exportStar(require("./idempotency"), exports);
20
+ __exportStar(require("./lock"), exports);
21
+ __exportStar(require("./nonce"), exports);
22
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,0CAAwB;AACxB,4CAA0B;AAC1B,gDAA8B;AAC9B,yCAAuB;AACvB,0CAAwB"}
package/dist/lock.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { RedisClientInterface } from './types';
2
+ export interface RedisLockResult {
3
+ acquired: boolean;
4
+ token: string;
5
+ }
6
+ export interface RedisLockOptions {
7
+ keyPrefix?: string;
8
+ strictAtomicity?: boolean;
9
+ }
10
+ export declare class RedisLock {
11
+ private client;
12
+ private keyPrefix;
13
+ private strictAtomicity;
14
+ constructor(client: RedisClientInterface, optionsOrPrefix?: string | RedisLockOptions);
15
+ private fullKey;
16
+ /**
17
+ * Attempts to acquire an atomic distributed lock for single-flight coordination.
18
+ */
19
+ acquire(lockKey: string, ttlMs?: number, customToken?: string): Promise<RedisLockResult>;
20
+ /**
21
+ * Releases the lock safely only if the token matches.
22
+ */
23
+ release(lockKey: string, token: string): Promise<boolean>;
24
+ /**
25
+ * Executes an async operation with single-flight lock protection.
26
+ */
27
+ withLock<T>(lockKey: string, fn: () => Promise<T>, ttlMs?: number): Promise<T>;
28
+ }
29
+ //# sourceMappingURL=lock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock.d.ts","sourceRoot":"","sources":["../src/lock.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAE/C,MAAM,WAAW,eAAe;IAC5B,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,qBAAa,SAAS;IAClB,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,eAAe,CAAU;gBAErB,MAAM,EAAE,oBAAoB,EAAE,eAAe,GAAE,MAAM,GAAG,gBAAgC;IAcpG,OAAO,CAAC,OAAO;IAIf;;OAEG;IACG,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,MAAc,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAUrG;;OAEG;IACG,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA2B/D;;OAEG;IACG,QAAQ,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,GAAE,MAAc,GAAG,OAAO,CAAC,CAAC,CAAC;CAW9F"}
package/dist/lock.js ADDED
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RedisLock = void 0;
4
+ const crypto_1 = require("crypto");
5
+ class RedisLock {
6
+ client;
7
+ keyPrefix;
8
+ strictAtomicity;
9
+ constructor(client, optionsOrPrefix = 'siglo:lock:') {
10
+ if (!client) {
11
+ throw new Error('RedisLock requires a valid Redis client instance.');
12
+ }
13
+ this.client = client;
14
+ if (typeof optionsOrPrefix === 'string') {
15
+ this.keyPrefix = optionsOrPrefix;
16
+ this.strictAtomicity = false;
17
+ }
18
+ else {
19
+ this.keyPrefix = optionsOrPrefix.keyPrefix || 'siglo:lock:';
20
+ this.strictAtomicity = optionsOrPrefix.strictAtomicity ?? false;
21
+ }
22
+ }
23
+ fullKey(key) {
24
+ return `${this.keyPrefix}${key}`;
25
+ }
26
+ /**
27
+ * Attempts to acquire an atomic distributed lock for single-flight coordination.
28
+ */
29
+ async acquire(lockKey, ttlMs = 10000, customToken) {
30
+ const key = this.fullKey(lockKey);
31
+ const token = customToken || `lock_${Date.now()}_${(0, crypto_1.randomBytes)(16).toString('hex')}`;
32
+ const res = await this.client.set(key, token, 'PX', ttlMs, 'NX');
33
+ const acquired = res === 'OK';
34
+ return { acquired, token };
35
+ }
36
+ /**
37
+ * Releases the lock safely only if the token matches.
38
+ */
39
+ async release(lockKey, token) {
40
+ const key = this.fullKey(lockKey);
41
+ if (this.client.eval) {
42
+ const lua = `
43
+ if redis.call("get", KEYS[1]) == ARGV[1] then
44
+ return redis.call("del", KEYS[1])
45
+ else
46
+ return 0
47
+ end
48
+ `;
49
+ const res = await this.client.eval(lua, 1, key, token);
50
+ return res === 1;
51
+ }
52
+ if (this.strictAtomicity) {
53
+ throw new Error('RedisLock: client.eval is required for atomic release to prevent TOCTOU race condition.');
54
+ }
55
+ const currentVal = await this.client.get(key);
56
+ if (currentVal === token) {
57
+ await this.client.del(key);
58
+ return true;
59
+ }
60
+ return false;
61
+ }
62
+ /**
63
+ * Executes an async operation with single-flight lock protection.
64
+ */
65
+ async withLock(lockKey, fn, ttlMs = 10000) {
66
+ const { acquired, token } = await this.acquire(lockKey, ttlMs);
67
+ if (!acquired) {
68
+ throw new Error(`Failed to acquire lock for key: ${lockKey} (in-use or concurrent execution)`);
69
+ }
70
+ try {
71
+ return await fn();
72
+ }
73
+ finally {
74
+ await this.release(lockKey, token).catch(() => { });
75
+ }
76
+ }
77
+ }
78
+ exports.RedisLock = RedisLock;
79
+ //# sourceMappingURL=lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock.js","sourceRoot":"","sources":["../src/lock.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import { RedisNonceStoreOptions } from './types';
2
+ /**
3
+ * Distributed Redis nonce / signature deduplication store for webhook replay attack prevention.
4
+ * Uses atomic Redis `SET ... PX ... NX` to enforce single-use tokens within the tolerance window.
5
+ */
6
+ export declare class RedisNonceStore {
7
+ private client;
8
+ private keyPrefix;
9
+ constructor(options: RedisNonceStoreOptions);
10
+ /**
11
+ * Atomically claims a deduplication key with TTL.
12
+ * Returns true if claimed (first delivery attempt), or false if key already exists (replay).
13
+ */
14
+ claim(key: string, ttlSeconds: number): Promise<boolean>;
15
+ }
16
+ //# sourceMappingURL=nonce.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nonce.d.ts","sourceRoot":"","sources":["../src/nonce.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAEvE;;;GAGG;AACH,qBAAa,eAAe;IACxB,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,SAAS,CAAS;gBAEd,OAAO,EAAE,sBAAsB;IAQ3C;;;OAGG;IACG,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAMjE"}
package/dist/nonce.js ADDED
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RedisNonceStore = void 0;
4
+ /**
5
+ * Distributed Redis nonce / signature deduplication store for webhook replay attack prevention.
6
+ * Uses atomic Redis `SET ... PX ... NX` to enforce single-use tokens within the tolerance window.
7
+ */
8
+ class RedisNonceStore {
9
+ client;
10
+ keyPrefix;
11
+ constructor(options) {
12
+ if (!options || !options.client) {
13
+ throw new Error('RedisNonceStore requires a valid Redis client instance.');
14
+ }
15
+ this.client = options.client;
16
+ this.keyPrefix = options.keyPrefix || 'siglo:wh:dedup:';
17
+ }
18
+ /**
19
+ * Atomically claims a deduplication key with TTL.
20
+ * Returns true if claimed (first delivery attempt), or false if key already exists (replay).
21
+ */
22
+ async claim(key, ttlSeconds) {
23
+ const fullKey = `${this.keyPrefix}${key}`;
24
+ const ttlMs = Math.max(1, Math.floor(ttlSeconds * 1000));
25
+ const res = await this.client.set(fullKey, '1', 'PX', ttlMs, 'NX');
26
+ return res === 'OK';
27
+ }
28
+ }
29
+ exports.RedisNonceStore = RedisNonceStore;
30
+ //# sourceMappingURL=nonce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nonce.js","sourceRoot":"","sources":["../src/nonce.ts"],"names":[],"mappings":";;;AAEA;;;GAGG;AACH,MAAa,eAAe;IAChB,MAAM,CAAuB;IAC7B,SAAS,CAAS;IAE1B,YAAY,OAA+B;QACvC,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;QAC/E,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;QAC7B,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,iBAAiB,CAAC;IAC5D,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,KAAK,CAAC,GAAW,EAAE,UAAkB;QACvC,MAAM,OAAO,GAAG,GAAG,IAAI,CAAC,SAAS,GAAG,GAAG,EAAE,CAAC;QAC1C,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC,CAAC;QACzD,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;QACnE,OAAO,GAAG,KAAK,IAAI,CAAC;IACxB,CAAC;CACJ;AAtBD,0CAsBC"}
@@ -0,0 +1,27 @@
1
+ export interface RedisClientInterface {
2
+ get(key: string): Promise<string | null>;
3
+ set(key: string, value: string, ...args: any[]): Promise<'OK' | string | null>;
4
+ del(key: string | string[]): Promise<number>;
5
+ hset(key: string, field: string | Record<string, string>, value?: string): Promise<number>;
6
+ hgetall?(key: string): Promise<Record<string, string>>;
7
+ expire?(key: string, seconds: number): Promise<number>;
8
+ pexpire?(key: string, milliseconds: number): Promise<number>;
9
+ eval?(script: string, numKeys: number, ...args: (string | number)[]): Promise<any>;
10
+ }
11
+ export interface RedisStorageAdapterOptions {
12
+ client: RedisClientInterface;
13
+ keyPrefix?: string;
14
+ cacheTtlMs?: number;
15
+ checkpointTtlMs?: number;
16
+ recordContextChanges?: boolean;
17
+ }
18
+ export interface RedisIdempotencyOptions {
19
+ client: RedisClientInterface;
20
+ keyPrefix?: string;
21
+ defaultTtlMs?: number;
22
+ }
23
+ export interface RedisNonceStoreOptions {
24
+ client: RedisClientInterface;
25
+ keyPrefix?: string;
26
+ }
27
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,oBAAoB;IACjC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,IAAI,GAAG,MAAM,GAAG,IAAI,CAAC,CAAC;IAC/E,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7C,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3F,OAAO,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACvD,MAAM,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACvD,OAAO,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7D,IAAI,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;CACtF;AAED,MAAM,WAAW,0BAA0B;IACvC,MAAM,EAAE,oBAAoB,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED,MAAM,WAAW,uBAAuB;IACpC,MAAM,EAAE,oBAAoB,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,sBAAsB;IACnC,MAAM,EAAE,oBAAoB,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB"}
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,61 @@
1
1
  {
2
2
  "name": "@siglo/redis",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Production Redis distributed cache, durable checkpoint, and idempotency adapter for Siglo workflows",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js",
11
+ "require": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "publishConfig": {
22
+ "access": "public",
23
+ "registry": "https://registry.npmjs.org/"
24
+ },
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.json",
27
+ "prepublishOnly": "npm run build",
28
+ "test": "npm run build && mocha -r tsx 'src/**/*.test.ts'"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/jsoftgem/siglo.git",
33
+ "directory": "packages/redis"
34
+ },
35
+ "author": "Jerico de Guzman",
36
+ "license": "MIT",
37
+ "bugs": {
38
+ "url": "https://github.com/jsoftgem/siglo/issues"
39
+ },
40
+ "homepage": "https://github.com/jsoftgem/siglo#readme",
41
+ "dependencies": {
42
+ "@siglo/core": "^1.0.0"
43
+ },
44
+ "peerDependencies": {
45
+ "ioredis": "^5.0.0 || ^6.0.0"
46
+ },
47
+ "peerDependenciesMeta": {
48
+ "ioredis": {
49
+ "optional": true
50
+ }
51
+ },
52
+ "devDependencies": {
53
+ "@types/chai": "^5.2.3",
54
+ "@types/mocha": "^10.0.10",
55
+ "@types/node": "^26.6.5",
56
+ "chai": "^3.5.0",
57
+ "mocha": "^12.0.3",
58
+ "tsx": "^4.23.15",
59
+ "typescript": "~5.7.0"
60
+ }
61
+ }