@siglo/redis 0.0.0-stage → 1.0.1
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 +21 -0
- package/README.md +226 -2
- package/dist/adapter.d.ts +31 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +142 -0
- package/dist/adapter.js.map +1 -0
- package/dist/idempotency.d.ts +15 -0
- package/dist/idempotency.d.ts.map +1 -0
- package/dist/idempotency.js +68 -0
- package/dist/idempotency.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/lock.d.ts +29 -0
- package/dist/lock.d.ts.map +1 -0
- package/dist/lock.js +79 -0
- package/dist/lock.js.map +1 -0
- package/dist/nonce.d.ts +16 -0
- package/dist/nonce.d.ts.map +1 -0
- package/dist/nonce.js +30 -0
- package/dist/nonce.js.map +1 -0
- package/dist/types.d.ts +27 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +59 -4
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
|
-
#
|
|
1
|
+
# @siglo/redis Adapter Guide
|
|
2
2
|
|
|
3
|
-
|
|
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. **`get(key)` / `getResult(key)`**:
|
|
117
|
+
- Returns the deserialized result for completed keys (`get` is an alias delegating to `getResult`).
|
|
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"}
|
package/dist/adapter.js
ADDED
|
@@ -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,15 @@
|
|
|
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
|
+
get(key: string): Promise<unknown | undefined>;
|
|
14
|
+
}
|
|
15
|
+
//# 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;IAapD,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC;CAGvD"}
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
async get(key) {
|
|
64
|
+
return this.getResult(key);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
exports.RedisIdempotencyStore = RedisIdempotencyStore;
|
|
68
|
+
//# sourceMappingURL=idempotency.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idempotency.js","sourceRoot":"","sources":["../src/idempotency.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
package/dist/lock.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.js","sourceRoot":"","sources":["../src/lock.ts"],"names":[],"mappings":";;;AAAA,mCAAqC;AAarC,MAAa,SAAS;IACV,MAAM,CAAuB;IAC7B,SAAS,CAAS;IAClB,eAAe,CAAU;IAEjC,YAAY,MAA4B,EAAE,kBAA6C,aAAa;QAChG,IAAI,CAAC,MAAM,EAAE,CAAC;YACV,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAC;QACzE,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,OAAO,eAAe,KAAK,QAAQ,EAAE,CAAC;YACtC,IAAI,CAAC,SAAS,GAAG,eAAe,CAAC;YACjC,IAAI,CAAC,eAAe,GAAG,KAAK,CAAC;QACjC,CAAC;aAAM,CAAC;YACJ,IAAI,CAAC,SAAS,GAAG,eAAe,CAAC,SAAS,IAAI,aAAa,CAAC;YAC5D,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC,eAAe,IAAI,KAAK,CAAC;QACpE,CAAC;IACL,CAAC;IAEO,OAAO,CAAC,GAAW;QACvB,OAAO,GAAG,IAAI,CAAC,SAAS,GAAG,GAAG,EAAE,CAAC;IACrC,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,OAAO,CAAC,OAAe,EAAE,QAAgB,KAAK,EAAE,WAAoB;QACtE,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAClC,MAAM,KAAK,GAAG,WAAW,IAAI,QAAQ,IAAI,CAAC,GAAG,EAAE,IAAI,IAAA,oBAAW,EAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QAErF,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;QACjE,MAAM,QAAQ,GAAG,GAAG,KAAK,IAAI,CAAC;QAE9B,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IAC/B,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,OAAO,CAAC,OAAe,EAAE,KAAa;QACxC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAElC,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YACnB,MAAM,GAAG,GAAG;;;;;;aAMX,CAAC;YACF,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;YACvD,OAAO,GAAG,KAAK,CAAC,CAAC;QACrB,CAAC;QAED,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CAAC,yFAAyF,CAAC,CAAC;QAC/G,CAAC;QAED,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC9C,IAAI,UAAU,KAAK,KAAK,EAAE,CAAC;YACvB,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAC3B,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,OAAO,KAAK,CAAC;IACjB,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,QAAQ,CAAI,OAAe,EAAE,EAAoB,EAAE,QAAgB,KAAK;QAC1E,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAC/D,IAAI,CAAC,QAAQ,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CAAC,mCAAmC,OAAO,mCAAmC,CAAC,CAAC;QACnG,CAAC;QACD,IAAI,CAAC;YACD,OAAO,MAAM,EAAE,EAAE,CAAC;QACtB,CAAC;gBAAS,CAAC;YACP,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACvD,CAAC;IACL,CAAC;CACJ;AAhFD,8BAgFC"}
|
package/dist/nonce.d.ts
ADDED
|
@@ -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"}
|
package/dist/types.d.ts
ADDED
|
@@ -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 @@
|
|
|
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": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.1",
|
|
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
|
+
}
|