@saga-bus/store-redis 0.1.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 +21 -0
- package/README.md +218 -0
- package/dist/index.cjs +212 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +64 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +185 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Dean Foran
|
|
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
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# @saga-bus/store-redis
|
|
2
|
+
|
|
3
|
+
Redis-backed saga store for saga-bus with optimistic concurrency.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @saga-bus/store-redis ioredis
|
|
9
|
+
# or
|
|
10
|
+
pnpm add @saga-bus/store-redis ioredis
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **Fast**: Sub-millisecond read/write operations
|
|
16
|
+
- **Optimistic Concurrency**: WATCH/MULTI for conflict detection
|
|
17
|
+
- **TTL Support**: Automatic cleanup of completed sagas
|
|
18
|
+
- **Index Lookup**: Find sagas by ID or correlation ID
|
|
19
|
+
- **Clustering**: Works with Redis Cluster
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { createBus } from "@saga-bus/core";
|
|
25
|
+
import { RedisSagaStore } from "@saga-bus/store-redis";
|
|
26
|
+
|
|
27
|
+
const store = new RedisSagaStore({
|
|
28
|
+
connection: { host: "localhost", port: 6379 },
|
|
29
|
+
completedTtlSeconds: 86400, // Auto-cleanup after 1 day
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
await store.initialize();
|
|
33
|
+
|
|
34
|
+
const bus = createBus({
|
|
35
|
+
store,
|
|
36
|
+
// ... other config
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await bus.start();
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Configuration
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
interface RedisSagaStoreOptions {
|
|
46
|
+
/** Existing Redis client */
|
|
47
|
+
redis?: Redis;
|
|
48
|
+
|
|
49
|
+
/** Connection options for creating new client */
|
|
50
|
+
connection?: RedisOptions;
|
|
51
|
+
|
|
52
|
+
/** Key prefix for all saga keys (default: "saga-bus:") */
|
|
53
|
+
keyPrefix?: string;
|
|
54
|
+
|
|
55
|
+
/** TTL in seconds for completed sagas (0 = no expiry) */
|
|
56
|
+
completedTtlSeconds?: number;
|
|
57
|
+
|
|
58
|
+
/** TTL in seconds for all sagas (0 = no expiry) */
|
|
59
|
+
defaultTtlSeconds?: number;
|
|
60
|
+
|
|
61
|
+
/** Maximum retries for optimistic locking conflicts (default: 3) */
|
|
62
|
+
maxRetries?: number;
|
|
63
|
+
|
|
64
|
+
/** Delay between retries in milliseconds (default: 100) */
|
|
65
|
+
retryDelayMs?: number;
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Examples
|
|
70
|
+
|
|
71
|
+
### Basic Usage
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { RedisSagaStore } from "@saga-bus/store-redis";
|
|
75
|
+
|
|
76
|
+
const store = new RedisSagaStore({
|
|
77
|
+
connection: { host: "localhost", port: 6379 },
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await store.initialize();
|
|
81
|
+
|
|
82
|
+
// Find by correlation ID
|
|
83
|
+
const state = await store.findByCorrelationId("OrderSaga", "order-123");
|
|
84
|
+
|
|
85
|
+
// Find by saga ID
|
|
86
|
+
const stateById = await store.findById("OrderSaga", "saga-456");
|
|
87
|
+
|
|
88
|
+
// Save state
|
|
89
|
+
await store.save("OrderSaga", {
|
|
90
|
+
id: "saga-456",
|
|
91
|
+
correlationId: "order-123",
|
|
92
|
+
status: "running",
|
|
93
|
+
data: { orderId: "order-123" },
|
|
94
|
+
metadata: { /* ... */ },
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Delete
|
|
98
|
+
await store.delete("OrderSaga", "order-123");
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### With Existing Redis Client
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { Redis } from "ioredis";
|
|
105
|
+
|
|
106
|
+
const redis = new Redis({
|
|
107
|
+
host: "localhost",
|
|
108
|
+
port: 6379,
|
|
109
|
+
password: "secret",
|
|
110
|
+
db: 1,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const store = new RedisSagaStore({
|
|
114
|
+
redis,
|
|
115
|
+
keyPrefix: "myapp:",
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### With TTL for Auto-Cleanup
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
const store = new RedisSagaStore({
|
|
123
|
+
connection: { host: "localhost", port: 6379 },
|
|
124
|
+
completedTtlSeconds: 86400, // Delete completed sagas after 24 hours
|
|
125
|
+
defaultTtlSeconds: 604800, // Delete all sagas after 7 days
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Redis Cluster
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { Cluster } from "ioredis";
|
|
133
|
+
|
|
134
|
+
const cluster = new Cluster([
|
|
135
|
+
{ host: "redis-1", port: 6379 },
|
|
136
|
+
{ host: "redis-2", port: 6379 },
|
|
137
|
+
{ host: "redis-3", port: 6379 },
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
const store = new RedisSagaStore({
|
|
141
|
+
redis: cluster as any,
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Key Structure
|
|
146
|
+
|
|
147
|
+
The store uses the following key structure:
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
{prefix}saga:{sagaName}:{correlationId} -> JSON serialized state
|
|
151
|
+
{prefix}saga:{sagaName}:idx:id:{sagaId} -> correlation ID (index)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Example:
|
|
155
|
+
```
|
|
156
|
+
saga-bus:saga:OrderSaga:order-123 -> {"id":"saga-456",...}
|
|
157
|
+
saga-bus:saga:OrderSaga:idx:id:saga-456 -> "order-123"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Optimistic Concurrency
|
|
161
|
+
|
|
162
|
+
The store uses Redis WATCH/MULTI for optimistic locking:
|
|
163
|
+
|
|
164
|
+
1. `WATCH` the key before reading
|
|
165
|
+
2. Read current state and check version
|
|
166
|
+
3. `MULTI` to start transaction
|
|
167
|
+
4. `SET` new state
|
|
168
|
+
5. `EXEC` - fails if key was modified
|
|
169
|
+
|
|
170
|
+
If a conflict is detected, the operation is retried up to `maxRetries` times.
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
const store = new RedisSagaStore({
|
|
174
|
+
connection: { host: "localhost", port: 6379 },
|
|
175
|
+
maxRetries: 5, // More retries for high-contention scenarios
|
|
176
|
+
retryDelayMs: 50, // Shorter delay between retries
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Performance Considerations
|
|
181
|
+
|
|
182
|
+
1. **Use Key Prefixes**: Helps with Redis SCAN operations and debugging
|
|
183
|
+
2. **Set TTLs**: Prevents unbounded growth of saga data
|
|
184
|
+
3. **Connection Pooling**: Reuse Redis connections across stores
|
|
185
|
+
4. **Clustering**: Use Redis Cluster for horizontal scaling
|
|
186
|
+
|
|
187
|
+
## Error Handling
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
try {
|
|
191
|
+
await store.save("OrderSaga", state);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (error.message.includes("Optimistic concurrency conflict")) {
|
|
194
|
+
// State was modified by another process
|
|
195
|
+
// Reload and retry
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Testing
|
|
201
|
+
|
|
202
|
+
For testing, you can run Redis locally:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
docker run -p 6379:6379 redis:latest
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Or use an in-memory store for unit tests:
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
import { InMemorySagaStore } from "@saga-bus/core";
|
|
212
|
+
|
|
213
|
+
const testStore = new InMemorySagaStore();
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## License
|
|
217
|
+
|
|
218
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
RedisSagaStore: () => RedisSagaStore
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
|
|
27
|
+
// src/RedisSagaStore.ts
|
|
28
|
+
var import_ioredis = require("ioredis");
|
|
29
|
+
var import_core = require("@saga-bus/core");
|
|
30
|
+
var RedisSagaStore = class {
|
|
31
|
+
redis = null;
|
|
32
|
+
options;
|
|
33
|
+
ownsRedis;
|
|
34
|
+
constructor(options) {
|
|
35
|
+
if (!options.redis && !options.connection) {
|
|
36
|
+
throw new Error("Either redis or connection must be provided");
|
|
37
|
+
}
|
|
38
|
+
this.options = {
|
|
39
|
+
keyPrefix: "saga-bus:",
|
|
40
|
+
completedTtlSeconds: 0,
|
|
41
|
+
defaultTtlSeconds: 0,
|
|
42
|
+
maxRetries: 3,
|
|
43
|
+
retryDelayMs: 100,
|
|
44
|
+
...options
|
|
45
|
+
};
|
|
46
|
+
this.ownsRedis = !options.redis;
|
|
47
|
+
}
|
|
48
|
+
async initialize() {
|
|
49
|
+
if (this.options.redis) {
|
|
50
|
+
this.redis = this.options.redis;
|
|
51
|
+
} else {
|
|
52
|
+
this.redis = new import_ioredis.Redis(this.options.connection);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async close() {
|
|
56
|
+
if (this.ownsRedis && this.redis) {
|
|
57
|
+
await this.redis.quit();
|
|
58
|
+
}
|
|
59
|
+
this.redis = null;
|
|
60
|
+
}
|
|
61
|
+
async getById(sagaName, sagaId) {
|
|
62
|
+
if (!this.redis) throw new Error("Store not initialized");
|
|
63
|
+
const indexKey = this.buildIdIndexKey(sagaName, sagaId);
|
|
64
|
+
const correlationId = await this.redis.get(indexKey);
|
|
65
|
+
if (!correlationId) return null;
|
|
66
|
+
return this.getByCorrelationId(sagaName, correlationId);
|
|
67
|
+
}
|
|
68
|
+
async getByCorrelationId(sagaName, correlationId) {
|
|
69
|
+
if (!this.redis) throw new Error("Store not initialized");
|
|
70
|
+
const key = this.buildKey(sagaName, correlationId);
|
|
71
|
+
const data = await this.redis.get(key);
|
|
72
|
+
if (!data) return null;
|
|
73
|
+
return this.deserializeState(data);
|
|
74
|
+
}
|
|
75
|
+
async insert(sagaName, correlationId, state) {
|
|
76
|
+
if (!this.redis) throw new Error("Store not initialized");
|
|
77
|
+
const key = this.buildKey(sagaName, correlationId);
|
|
78
|
+
const indexKey = this.buildIdIndexKey(sagaName, state.metadata.sagaId);
|
|
79
|
+
const existing = await this.redis.get(key);
|
|
80
|
+
if (existing) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Saga ${sagaName} with correlation ID ${correlationId} already exists`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const serialized = this.serializeState(state);
|
|
86
|
+
let ttl = this.options.defaultTtlSeconds;
|
|
87
|
+
if (state.metadata.isCompleted && this.options.completedTtlSeconds > 0) {
|
|
88
|
+
ttl = this.options.completedTtlSeconds;
|
|
89
|
+
}
|
|
90
|
+
const multi = this.redis.multi();
|
|
91
|
+
if (ttl > 0) {
|
|
92
|
+
multi.setex(key, ttl, serialized);
|
|
93
|
+
multi.setex(indexKey, ttl, correlationId);
|
|
94
|
+
} else {
|
|
95
|
+
multi.set(key, serialized);
|
|
96
|
+
multi.set(indexKey, correlationId);
|
|
97
|
+
}
|
|
98
|
+
await multi.exec();
|
|
99
|
+
}
|
|
100
|
+
async update(sagaName, state, expectedVersion) {
|
|
101
|
+
if (!this.redis) throw new Error("Store not initialized");
|
|
102
|
+
const indexKey = this.buildIdIndexKey(sagaName, state.metadata.sagaId);
|
|
103
|
+
const correlationId = await this.redis.get(indexKey);
|
|
104
|
+
if (!correlationId) {
|
|
105
|
+
throw new Error(`Saga ${state.metadata.sagaId} not found`);
|
|
106
|
+
}
|
|
107
|
+
const key = this.buildKey(sagaName, correlationId);
|
|
108
|
+
for (let attempt = 0; attempt < this.options.maxRetries; attempt++) {
|
|
109
|
+
await this.redis.watch(key);
|
|
110
|
+
try {
|
|
111
|
+
const existing = await this.redis.get(key);
|
|
112
|
+
if (!existing) {
|
|
113
|
+
await this.redis.unwatch();
|
|
114
|
+
throw new Error(`Saga ${state.metadata.sagaId} not found`);
|
|
115
|
+
}
|
|
116
|
+
const currentState = this.deserializeState(existing);
|
|
117
|
+
if (currentState.metadata.version !== expectedVersion) {
|
|
118
|
+
await this.redis.unwatch();
|
|
119
|
+
throw new import_core.ConcurrencyError(
|
|
120
|
+
state.metadata.sagaId,
|
|
121
|
+
expectedVersion,
|
|
122
|
+
currentState.metadata.version
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const multi = this.redis.multi();
|
|
126
|
+
const serialized = this.serializeState(state);
|
|
127
|
+
let ttl = this.options.defaultTtlSeconds;
|
|
128
|
+
if (state.metadata.isCompleted && this.options.completedTtlSeconds > 0) {
|
|
129
|
+
ttl = this.options.completedTtlSeconds;
|
|
130
|
+
}
|
|
131
|
+
if (ttl > 0) {
|
|
132
|
+
multi.setex(key, ttl, serialized);
|
|
133
|
+
multi.setex(indexKey, ttl, correlationId);
|
|
134
|
+
} else {
|
|
135
|
+
multi.set(key, serialized);
|
|
136
|
+
multi.set(indexKey, correlationId);
|
|
137
|
+
}
|
|
138
|
+
const result = await multi.exec();
|
|
139
|
+
if (result === null) {
|
|
140
|
+
if (attempt < this.options.maxRetries - 1) {
|
|
141
|
+
await this.delay(this.options.retryDelayMs);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
throw new import_core.ConcurrencyError(
|
|
145
|
+
state.metadata.sagaId,
|
|
146
|
+
expectedVersion,
|
|
147
|
+
-1
|
|
148
|
+
// Unknown current version due to race
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
await this.redis.unwatch();
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async delete(sagaName, sagaId) {
|
|
159
|
+
if (!this.redis) throw new Error("Store not initialized");
|
|
160
|
+
const indexKey = this.buildIdIndexKey(sagaName, sagaId);
|
|
161
|
+
const correlationId = await this.redis.get(indexKey);
|
|
162
|
+
if (!correlationId) return;
|
|
163
|
+
const key = this.buildKey(sagaName, correlationId);
|
|
164
|
+
await this.redis.del(key, indexKey);
|
|
165
|
+
}
|
|
166
|
+
buildKey(sagaName, correlationId) {
|
|
167
|
+
return `${this.options.keyPrefix}saga:${sagaName}:${correlationId}`;
|
|
168
|
+
}
|
|
169
|
+
buildIdIndexKey(sagaName, sagaId) {
|
|
170
|
+
return `${this.options.keyPrefix}saga:${sagaName}:idx:id:${sagaId}`;
|
|
171
|
+
}
|
|
172
|
+
serializeState(state) {
|
|
173
|
+
return JSON.stringify({
|
|
174
|
+
...state,
|
|
175
|
+
metadata: {
|
|
176
|
+
...state.metadata,
|
|
177
|
+
createdAt: state.metadata.createdAt.toISOString(),
|
|
178
|
+
updatedAt: state.metadata.updatedAt.toISOString(),
|
|
179
|
+
archivedAt: state.metadata.archivedAt?.toISOString() ?? null,
|
|
180
|
+
timeoutExpiresAt: state.metadata.timeoutExpiresAt?.toISOString() ?? null
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
deserializeState(data) {
|
|
185
|
+
const parsed = JSON.parse(data);
|
|
186
|
+
return {
|
|
187
|
+
...parsed,
|
|
188
|
+
metadata: {
|
|
189
|
+
...parsed.metadata,
|
|
190
|
+
createdAt: new Date(parsed.metadata.createdAt),
|
|
191
|
+
updatedAt: new Date(parsed.metadata.updatedAt),
|
|
192
|
+
archivedAt: parsed.metadata.archivedAt ? new Date(parsed.metadata.archivedAt) : null,
|
|
193
|
+
timeoutExpiresAt: parsed.metadata.timeoutExpiresAt ? new Date(parsed.metadata.timeoutExpiresAt) : null
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
delay(ms) {
|
|
198
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
199
|
+
}
|
|
200
|
+
// ============ Query Helpers ============
|
|
201
|
+
/**
|
|
202
|
+
* Get the underlying Redis client for advanced operations.
|
|
203
|
+
*/
|
|
204
|
+
getRedis() {
|
|
205
|
+
return this.redis;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
209
|
+
0 && (module.exports = {
|
|
210
|
+
RedisSagaStore
|
|
211
|
+
});
|
|
212
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/RedisSagaStore.ts"],"sourcesContent":["export { RedisSagaStore } from \"./RedisSagaStore.js\";\nexport type { RedisSagaStoreOptions } from \"./types.js\";\n","import { Redis } from \"ioredis\";\nimport type { SagaStore, SagaState } from \"@saga-bus/core\";\nimport { ConcurrencyError } from \"@saga-bus/core\";\nimport type { RedisSagaStoreOptions } from \"./types.js\";\n\n/**\n * Redis-backed saga store for saga-bus.\n *\n * Uses WATCH/MULTI for optimistic concurrency control.\n *\n * @example\n * ```typescript\n * import { RedisSagaStore } from \"@saga-bus/store-redis\";\n *\n * const store = new RedisSagaStore<OrderState>({\n * connection: { host: \"localhost\", port: 6379 },\n * completedTtlSeconds: 86400, // 1 day\n * });\n *\n * await store.initialize();\n * ```\n */\nexport class RedisSagaStore<TState extends SagaState>\n implements SagaStore<TState>\n{\n private redis: Redis | null = null;\n private readonly options: Required<\n Pick<\n RedisSagaStoreOptions,\n | \"keyPrefix\"\n | \"completedTtlSeconds\"\n | \"defaultTtlSeconds\"\n | \"maxRetries\"\n | \"retryDelayMs\"\n >\n > &\n RedisSagaStoreOptions;\n private readonly ownsRedis: boolean;\n\n constructor(options: RedisSagaStoreOptions) {\n if (!options.redis && !options.connection) {\n throw new Error(\"Either redis or connection must be provided\");\n }\n\n this.options = {\n keyPrefix: \"saga-bus:\",\n completedTtlSeconds: 0,\n defaultTtlSeconds: 0,\n maxRetries: 3,\n retryDelayMs: 100,\n ...options,\n };\n\n this.ownsRedis = !options.redis;\n }\n\n async initialize(): Promise<void> {\n if (this.options.redis) {\n this.redis = this.options.redis;\n } else {\n this.redis = new Redis(this.options.connection!);\n }\n }\n\n async close(): Promise<void> {\n if (this.ownsRedis && this.redis) {\n await this.redis.quit();\n }\n this.redis = null;\n }\n\n async getById(sagaName: string, sagaId: string): Promise<TState | null> {\n if (!this.redis) throw new Error(\"Store not initialized\");\n\n // Lookup correlation ID from index\n const indexKey = this.buildIdIndexKey(sagaName, sagaId);\n const correlationId = await this.redis.get(indexKey);\n\n if (!correlationId) return null;\n\n return this.getByCorrelationId(sagaName, correlationId);\n }\n\n async getByCorrelationId(\n sagaName: string,\n correlationId: string\n ): Promise<TState | null> {\n if (!this.redis) throw new Error(\"Store not initialized\");\n\n const key = this.buildKey(sagaName, correlationId);\n const data = await this.redis.get(key);\n\n if (!data) return null;\n\n return this.deserializeState(data);\n }\n\n async insert(\n sagaName: string,\n correlationId: string,\n state: TState\n ): Promise<void> {\n if (!this.redis) throw new Error(\"Store not initialized\");\n\n const key = this.buildKey(sagaName, correlationId);\n const indexKey = this.buildIdIndexKey(sagaName, state.metadata.sagaId);\n\n // Check if saga already exists\n const existing = await this.redis.get(key);\n if (existing) {\n throw new Error(\n `Saga ${sagaName} with correlation ID ${correlationId} already exists`\n );\n }\n\n // Serialize and save\n const serialized = this.serializeState(state);\n\n // Determine TTL\n let ttl = this.options.defaultTtlSeconds;\n if (state.metadata.isCompleted && this.options.completedTtlSeconds > 0) {\n ttl = this.options.completedTtlSeconds;\n }\n\n const multi = this.redis.multi();\n\n if (ttl > 0) {\n multi.setex(key, ttl, serialized);\n multi.setex(indexKey, ttl, correlationId);\n } else {\n multi.set(key, serialized);\n multi.set(indexKey, correlationId);\n }\n\n await multi.exec();\n }\n\n async update(\n sagaName: string,\n state: TState,\n expectedVersion: number\n ): Promise<void> {\n if (!this.redis) throw new Error(\"Store not initialized\");\n\n // We need to find the correlation ID for this saga\n const indexKey = this.buildIdIndexKey(sagaName, state.metadata.sagaId);\n const correlationId = await this.redis.get(indexKey);\n\n if (!correlationId) {\n throw new Error(`Saga ${state.metadata.sagaId} not found`);\n }\n\n const key = this.buildKey(sagaName, correlationId);\n\n for (let attempt = 0; attempt < this.options.maxRetries; attempt++) {\n // Watch the key for changes\n await this.redis.watch(key);\n\n try {\n // Check current version\n const existing = await this.redis.get(key);\n if (!existing) {\n await this.redis.unwatch();\n throw new Error(`Saga ${state.metadata.sagaId} not found`);\n }\n\n const currentState = this.deserializeState(existing);\n if (currentState.metadata.version !== expectedVersion) {\n await this.redis.unwatch();\n throw new ConcurrencyError(\n state.metadata.sagaId,\n expectedVersion,\n currentState.metadata.version\n );\n }\n\n // Start transaction\n const multi = this.redis.multi();\n\n // Serialize and save\n const serialized = this.serializeState(state);\n\n // Determine TTL\n let ttl = this.options.defaultTtlSeconds;\n if (state.metadata.isCompleted && this.options.completedTtlSeconds > 0) {\n ttl = this.options.completedTtlSeconds;\n }\n\n if (ttl > 0) {\n multi.setex(key, ttl, serialized);\n multi.setex(indexKey, ttl, correlationId);\n } else {\n multi.set(key, serialized);\n multi.set(indexKey, correlationId);\n }\n\n // Execute transaction\n const result = await multi.exec();\n\n if (result === null) {\n // Transaction aborted due to WATCH - retry\n if (attempt < this.options.maxRetries - 1) {\n await this.delay(this.options.retryDelayMs);\n continue;\n }\n throw new ConcurrencyError(\n state.metadata.sagaId,\n expectedVersion,\n -1 // Unknown current version due to race\n );\n }\n\n return; // Success\n } catch (error) {\n await this.redis.unwatch();\n throw error;\n }\n }\n }\n\n async delete(sagaName: string, sagaId: string): Promise<void> {\n if (!this.redis) throw new Error(\"Store not initialized\");\n\n // Lookup correlation ID from index\n const indexKey = this.buildIdIndexKey(sagaName, sagaId);\n const correlationId = await this.redis.get(indexKey);\n\n if (!correlationId) return;\n\n const key = this.buildKey(sagaName, correlationId);\n await this.redis.del(key, indexKey);\n }\n\n private buildKey(sagaName: string, correlationId: string): string {\n return `${this.options.keyPrefix}saga:${sagaName}:${correlationId}`;\n }\n\n private buildIdIndexKey(sagaName: string, sagaId: string): string {\n return `${this.options.keyPrefix}saga:${sagaName}:idx:id:${sagaId}`;\n }\n\n private serializeState(state: TState): string {\n return JSON.stringify({\n ...state,\n metadata: {\n ...state.metadata,\n createdAt: state.metadata.createdAt.toISOString(),\n updatedAt: state.metadata.updatedAt.toISOString(),\n archivedAt: state.metadata.archivedAt?.toISOString() ?? null,\n timeoutExpiresAt: state.metadata.timeoutExpiresAt?.toISOString() ?? null,\n },\n });\n }\n\n private deserializeState(data: string): TState {\n const parsed = JSON.parse(data);\n return {\n ...parsed,\n metadata: {\n ...parsed.metadata,\n createdAt: new Date(parsed.metadata.createdAt),\n updatedAt: new Date(parsed.metadata.updatedAt),\n archivedAt: parsed.metadata.archivedAt\n ? new Date(parsed.metadata.archivedAt)\n : null,\n timeoutExpiresAt: parsed.metadata.timeoutExpiresAt\n ? new Date(parsed.metadata.timeoutExpiresAt)\n : null,\n },\n } as TState;\n }\n\n private delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n\n // ============ Query Helpers ============\n\n /**\n * Get the underlying Redis client for advanced operations.\n */\n getRedis(): Redis | null {\n return this.redis;\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Redis, RedisOptions } from 'ioredis';
|
|
2
|
+
import { SagaState, SagaStore } from '@saga-bus/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Configuration options for the Redis saga store.
|
|
6
|
+
*/
|
|
7
|
+
interface RedisSagaStoreOptions {
|
|
8
|
+
/** Existing Redis client */
|
|
9
|
+
redis?: Redis;
|
|
10
|
+
/** Connection options for creating new client */
|
|
11
|
+
connection?: RedisOptions;
|
|
12
|
+
/** Key prefix for all saga keys (default: "saga-bus:") */
|
|
13
|
+
keyPrefix?: string;
|
|
14
|
+
/** TTL in seconds for completed sagas (0 = no expiry, default: 0) */
|
|
15
|
+
completedTtlSeconds?: number;
|
|
16
|
+
/** TTL in seconds for all sagas (0 = no expiry, default: 0) */
|
|
17
|
+
defaultTtlSeconds?: number;
|
|
18
|
+
/** Maximum retries for optimistic locking conflicts (default: 3) */
|
|
19
|
+
maxRetries?: number;
|
|
20
|
+
/** Delay between retries in milliseconds (default: 100) */
|
|
21
|
+
retryDelayMs?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Redis-backed saga store for saga-bus.
|
|
26
|
+
*
|
|
27
|
+
* Uses WATCH/MULTI for optimistic concurrency control.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* import { RedisSagaStore } from "@saga-bus/store-redis";
|
|
32
|
+
*
|
|
33
|
+
* const store = new RedisSagaStore<OrderState>({
|
|
34
|
+
* connection: { host: "localhost", port: 6379 },
|
|
35
|
+
* completedTtlSeconds: 86400, // 1 day
|
|
36
|
+
* });
|
|
37
|
+
*
|
|
38
|
+
* await store.initialize();
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
declare class RedisSagaStore<TState extends SagaState> implements SagaStore<TState> {
|
|
42
|
+
private redis;
|
|
43
|
+
private readonly options;
|
|
44
|
+
private readonly ownsRedis;
|
|
45
|
+
constructor(options: RedisSagaStoreOptions);
|
|
46
|
+
initialize(): Promise<void>;
|
|
47
|
+
close(): Promise<void>;
|
|
48
|
+
getById(sagaName: string, sagaId: string): Promise<TState | null>;
|
|
49
|
+
getByCorrelationId(sagaName: string, correlationId: string): Promise<TState | null>;
|
|
50
|
+
insert(sagaName: string, correlationId: string, state: TState): Promise<void>;
|
|
51
|
+
update(sagaName: string, state: TState, expectedVersion: number): Promise<void>;
|
|
52
|
+
delete(sagaName: string, sagaId: string): Promise<void>;
|
|
53
|
+
private buildKey;
|
|
54
|
+
private buildIdIndexKey;
|
|
55
|
+
private serializeState;
|
|
56
|
+
private deserializeState;
|
|
57
|
+
private delay;
|
|
58
|
+
/**
|
|
59
|
+
* Get the underlying Redis client for advanced operations.
|
|
60
|
+
*/
|
|
61
|
+
getRedis(): Redis | null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { RedisSagaStore, type RedisSagaStoreOptions };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Redis, RedisOptions } from 'ioredis';
|
|
2
|
+
import { SagaState, SagaStore } from '@saga-bus/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Configuration options for the Redis saga store.
|
|
6
|
+
*/
|
|
7
|
+
interface RedisSagaStoreOptions {
|
|
8
|
+
/** Existing Redis client */
|
|
9
|
+
redis?: Redis;
|
|
10
|
+
/** Connection options for creating new client */
|
|
11
|
+
connection?: RedisOptions;
|
|
12
|
+
/** Key prefix for all saga keys (default: "saga-bus:") */
|
|
13
|
+
keyPrefix?: string;
|
|
14
|
+
/** TTL in seconds for completed sagas (0 = no expiry, default: 0) */
|
|
15
|
+
completedTtlSeconds?: number;
|
|
16
|
+
/** TTL in seconds for all sagas (0 = no expiry, default: 0) */
|
|
17
|
+
defaultTtlSeconds?: number;
|
|
18
|
+
/** Maximum retries for optimistic locking conflicts (default: 3) */
|
|
19
|
+
maxRetries?: number;
|
|
20
|
+
/** Delay between retries in milliseconds (default: 100) */
|
|
21
|
+
retryDelayMs?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Redis-backed saga store for saga-bus.
|
|
26
|
+
*
|
|
27
|
+
* Uses WATCH/MULTI for optimistic concurrency control.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* import { RedisSagaStore } from "@saga-bus/store-redis";
|
|
32
|
+
*
|
|
33
|
+
* const store = new RedisSagaStore<OrderState>({
|
|
34
|
+
* connection: { host: "localhost", port: 6379 },
|
|
35
|
+
* completedTtlSeconds: 86400, // 1 day
|
|
36
|
+
* });
|
|
37
|
+
*
|
|
38
|
+
* await store.initialize();
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
declare class RedisSagaStore<TState extends SagaState> implements SagaStore<TState> {
|
|
42
|
+
private redis;
|
|
43
|
+
private readonly options;
|
|
44
|
+
private readonly ownsRedis;
|
|
45
|
+
constructor(options: RedisSagaStoreOptions);
|
|
46
|
+
initialize(): Promise<void>;
|
|
47
|
+
close(): Promise<void>;
|
|
48
|
+
getById(sagaName: string, sagaId: string): Promise<TState | null>;
|
|
49
|
+
getByCorrelationId(sagaName: string, correlationId: string): Promise<TState | null>;
|
|
50
|
+
insert(sagaName: string, correlationId: string, state: TState): Promise<void>;
|
|
51
|
+
update(sagaName: string, state: TState, expectedVersion: number): Promise<void>;
|
|
52
|
+
delete(sagaName: string, sagaId: string): Promise<void>;
|
|
53
|
+
private buildKey;
|
|
54
|
+
private buildIdIndexKey;
|
|
55
|
+
private serializeState;
|
|
56
|
+
private deserializeState;
|
|
57
|
+
private delay;
|
|
58
|
+
/**
|
|
59
|
+
* Get the underlying Redis client for advanced operations.
|
|
60
|
+
*/
|
|
61
|
+
getRedis(): Redis | null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { RedisSagaStore, type RedisSagaStoreOptions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// src/RedisSagaStore.ts
|
|
2
|
+
import { Redis } from "ioredis";
|
|
3
|
+
import { ConcurrencyError } from "@saga-bus/core";
|
|
4
|
+
var RedisSagaStore = class {
|
|
5
|
+
redis = null;
|
|
6
|
+
options;
|
|
7
|
+
ownsRedis;
|
|
8
|
+
constructor(options) {
|
|
9
|
+
if (!options.redis && !options.connection) {
|
|
10
|
+
throw new Error("Either redis or connection must be provided");
|
|
11
|
+
}
|
|
12
|
+
this.options = {
|
|
13
|
+
keyPrefix: "saga-bus:",
|
|
14
|
+
completedTtlSeconds: 0,
|
|
15
|
+
defaultTtlSeconds: 0,
|
|
16
|
+
maxRetries: 3,
|
|
17
|
+
retryDelayMs: 100,
|
|
18
|
+
...options
|
|
19
|
+
};
|
|
20
|
+
this.ownsRedis = !options.redis;
|
|
21
|
+
}
|
|
22
|
+
async initialize() {
|
|
23
|
+
if (this.options.redis) {
|
|
24
|
+
this.redis = this.options.redis;
|
|
25
|
+
} else {
|
|
26
|
+
this.redis = new Redis(this.options.connection);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async close() {
|
|
30
|
+
if (this.ownsRedis && this.redis) {
|
|
31
|
+
await this.redis.quit();
|
|
32
|
+
}
|
|
33
|
+
this.redis = null;
|
|
34
|
+
}
|
|
35
|
+
async getById(sagaName, sagaId) {
|
|
36
|
+
if (!this.redis) throw new Error("Store not initialized");
|
|
37
|
+
const indexKey = this.buildIdIndexKey(sagaName, sagaId);
|
|
38
|
+
const correlationId = await this.redis.get(indexKey);
|
|
39
|
+
if (!correlationId) return null;
|
|
40
|
+
return this.getByCorrelationId(sagaName, correlationId);
|
|
41
|
+
}
|
|
42
|
+
async getByCorrelationId(sagaName, correlationId) {
|
|
43
|
+
if (!this.redis) throw new Error("Store not initialized");
|
|
44
|
+
const key = this.buildKey(sagaName, correlationId);
|
|
45
|
+
const data = await this.redis.get(key);
|
|
46
|
+
if (!data) return null;
|
|
47
|
+
return this.deserializeState(data);
|
|
48
|
+
}
|
|
49
|
+
async insert(sagaName, correlationId, state) {
|
|
50
|
+
if (!this.redis) throw new Error("Store not initialized");
|
|
51
|
+
const key = this.buildKey(sagaName, correlationId);
|
|
52
|
+
const indexKey = this.buildIdIndexKey(sagaName, state.metadata.sagaId);
|
|
53
|
+
const existing = await this.redis.get(key);
|
|
54
|
+
if (existing) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Saga ${sagaName} with correlation ID ${correlationId} already exists`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
const serialized = this.serializeState(state);
|
|
60
|
+
let ttl = this.options.defaultTtlSeconds;
|
|
61
|
+
if (state.metadata.isCompleted && this.options.completedTtlSeconds > 0) {
|
|
62
|
+
ttl = this.options.completedTtlSeconds;
|
|
63
|
+
}
|
|
64
|
+
const multi = this.redis.multi();
|
|
65
|
+
if (ttl > 0) {
|
|
66
|
+
multi.setex(key, ttl, serialized);
|
|
67
|
+
multi.setex(indexKey, ttl, correlationId);
|
|
68
|
+
} else {
|
|
69
|
+
multi.set(key, serialized);
|
|
70
|
+
multi.set(indexKey, correlationId);
|
|
71
|
+
}
|
|
72
|
+
await multi.exec();
|
|
73
|
+
}
|
|
74
|
+
async update(sagaName, state, expectedVersion) {
|
|
75
|
+
if (!this.redis) throw new Error("Store not initialized");
|
|
76
|
+
const indexKey = this.buildIdIndexKey(sagaName, state.metadata.sagaId);
|
|
77
|
+
const correlationId = await this.redis.get(indexKey);
|
|
78
|
+
if (!correlationId) {
|
|
79
|
+
throw new Error(`Saga ${state.metadata.sagaId} not found`);
|
|
80
|
+
}
|
|
81
|
+
const key = this.buildKey(sagaName, correlationId);
|
|
82
|
+
for (let attempt = 0; attempt < this.options.maxRetries; attempt++) {
|
|
83
|
+
await this.redis.watch(key);
|
|
84
|
+
try {
|
|
85
|
+
const existing = await this.redis.get(key);
|
|
86
|
+
if (!existing) {
|
|
87
|
+
await this.redis.unwatch();
|
|
88
|
+
throw new Error(`Saga ${state.metadata.sagaId} not found`);
|
|
89
|
+
}
|
|
90
|
+
const currentState = this.deserializeState(existing);
|
|
91
|
+
if (currentState.metadata.version !== expectedVersion) {
|
|
92
|
+
await this.redis.unwatch();
|
|
93
|
+
throw new ConcurrencyError(
|
|
94
|
+
state.metadata.sagaId,
|
|
95
|
+
expectedVersion,
|
|
96
|
+
currentState.metadata.version
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
const multi = this.redis.multi();
|
|
100
|
+
const serialized = this.serializeState(state);
|
|
101
|
+
let ttl = this.options.defaultTtlSeconds;
|
|
102
|
+
if (state.metadata.isCompleted && this.options.completedTtlSeconds > 0) {
|
|
103
|
+
ttl = this.options.completedTtlSeconds;
|
|
104
|
+
}
|
|
105
|
+
if (ttl > 0) {
|
|
106
|
+
multi.setex(key, ttl, serialized);
|
|
107
|
+
multi.setex(indexKey, ttl, correlationId);
|
|
108
|
+
} else {
|
|
109
|
+
multi.set(key, serialized);
|
|
110
|
+
multi.set(indexKey, correlationId);
|
|
111
|
+
}
|
|
112
|
+
const result = await multi.exec();
|
|
113
|
+
if (result === null) {
|
|
114
|
+
if (attempt < this.options.maxRetries - 1) {
|
|
115
|
+
await this.delay(this.options.retryDelayMs);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
throw new ConcurrencyError(
|
|
119
|
+
state.metadata.sagaId,
|
|
120
|
+
expectedVersion,
|
|
121
|
+
-1
|
|
122
|
+
// Unknown current version due to race
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
await this.redis.unwatch();
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async delete(sagaName, sagaId) {
|
|
133
|
+
if (!this.redis) throw new Error("Store not initialized");
|
|
134
|
+
const indexKey = this.buildIdIndexKey(sagaName, sagaId);
|
|
135
|
+
const correlationId = await this.redis.get(indexKey);
|
|
136
|
+
if (!correlationId) return;
|
|
137
|
+
const key = this.buildKey(sagaName, correlationId);
|
|
138
|
+
await this.redis.del(key, indexKey);
|
|
139
|
+
}
|
|
140
|
+
buildKey(sagaName, correlationId) {
|
|
141
|
+
return `${this.options.keyPrefix}saga:${sagaName}:${correlationId}`;
|
|
142
|
+
}
|
|
143
|
+
buildIdIndexKey(sagaName, sagaId) {
|
|
144
|
+
return `${this.options.keyPrefix}saga:${sagaName}:idx:id:${sagaId}`;
|
|
145
|
+
}
|
|
146
|
+
serializeState(state) {
|
|
147
|
+
return JSON.stringify({
|
|
148
|
+
...state,
|
|
149
|
+
metadata: {
|
|
150
|
+
...state.metadata,
|
|
151
|
+
createdAt: state.metadata.createdAt.toISOString(),
|
|
152
|
+
updatedAt: state.metadata.updatedAt.toISOString(),
|
|
153
|
+
archivedAt: state.metadata.archivedAt?.toISOString() ?? null,
|
|
154
|
+
timeoutExpiresAt: state.metadata.timeoutExpiresAt?.toISOString() ?? null
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
deserializeState(data) {
|
|
159
|
+
const parsed = JSON.parse(data);
|
|
160
|
+
return {
|
|
161
|
+
...parsed,
|
|
162
|
+
metadata: {
|
|
163
|
+
...parsed.metadata,
|
|
164
|
+
createdAt: new Date(parsed.metadata.createdAt),
|
|
165
|
+
updatedAt: new Date(parsed.metadata.updatedAt),
|
|
166
|
+
archivedAt: parsed.metadata.archivedAt ? new Date(parsed.metadata.archivedAt) : null,
|
|
167
|
+
timeoutExpiresAt: parsed.metadata.timeoutExpiresAt ? new Date(parsed.metadata.timeoutExpiresAt) : null
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
delay(ms) {
|
|
172
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
173
|
+
}
|
|
174
|
+
// ============ Query Helpers ============
|
|
175
|
+
/**
|
|
176
|
+
* Get the underlying Redis client for advanced operations.
|
|
177
|
+
*/
|
|
178
|
+
getRedis() {
|
|
179
|
+
return this.redis;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
export {
|
|
183
|
+
RedisSagaStore
|
|
184
|
+
};
|
|
185
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/RedisSagaStore.ts"],"sourcesContent":["import { Redis } from \"ioredis\";\nimport type { SagaStore, SagaState } from \"@saga-bus/core\";\nimport { ConcurrencyError } from \"@saga-bus/core\";\nimport type { RedisSagaStoreOptions } from \"./types.js\";\n\n/**\n * Redis-backed saga store for saga-bus.\n *\n * Uses WATCH/MULTI for optimistic concurrency control.\n *\n * @example\n * ```typescript\n * import { RedisSagaStore } from \"@saga-bus/store-redis\";\n *\n * const store = new RedisSagaStore<OrderState>({\n * connection: { host: \"localhost\", port: 6379 },\n * completedTtlSeconds: 86400, // 1 day\n * });\n *\n * await store.initialize();\n * ```\n */\nexport class RedisSagaStore<TState extends SagaState>\n implements SagaStore<TState>\n{\n private redis: Redis | null = null;\n private readonly options: Required<\n Pick<\n RedisSagaStoreOptions,\n | \"keyPrefix\"\n | \"completedTtlSeconds\"\n | \"defaultTtlSeconds\"\n | \"maxRetries\"\n | \"retryDelayMs\"\n >\n > &\n RedisSagaStoreOptions;\n private readonly ownsRedis: boolean;\n\n constructor(options: RedisSagaStoreOptions) {\n if (!options.redis && !options.connection) {\n throw new Error(\"Either redis or connection must be provided\");\n }\n\n this.options = {\n keyPrefix: \"saga-bus:\",\n completedTtlSeconds: 0,\n defaultTtlSeconds: 0,\n maxRetries: 3,\n retryDelayMs: 100,\n ...options,\n };\n\n this.ownsRedis = !options.redis;\n }\n\n async initialize(): Promise<void> {\n if (this.options.redis) {\n this.redis = this.options.redis;\n } else {\n this.redis = new Redis(this.options.connection!);\n }\n }\n\n async close(): Promise<void> {\n if (this.ownsRedis && this.redis) {\n await this.redis.quit();\n }\n this.redis = null;\n }\n\n async getById(sagaName: string, sagaId: string): Promise<TState | null> {\n if (!this.redis) throw new Error(\"Store not initialized\");\n\n // Lookup correlation ID from index\n const indexKey = this.buildIdIndexKey(sagaName, sagaId);\n const correlationId = await this.redis.get(indexKey);\n\n if (!correlationId) return null;\n\n return this.getByCorrelationId(sagaName, correlationId);\n }\n\n async getByCorrelationId(\n sagaName: string,\n correlationId: string\n ): Promise<TState | null> {\n if (!this.redis) throw new Error(\"Store not initialized\");\n\n const key = this.buildKey(sagaName, correlationId);\n const data = await this.redis.get(key);\n\n if (!data) return null;\n\n return this.deserializeState(data);\n }\n\n async insert(\n sagaName: string,\n correlationId: string,\n state: TState\n ): Promise<void> {\n if (!this.redis) throw new Error(\"Store not initialized\");\n\n const key = this.buildKey(sagaName, correlationId);\n const indexKey = this.buildIdIndexKey(sagaName, state.metadata.sagaId);\n\n // Check if saga already exists\n const existing = await this.redis.get(key);\n if (existing) {\n throw new Error(\n `Saga ${sagaName} with correlation ID ${correlationId} already exists`\n );\n }\n\n // Serialize and save\n const serialized = this.serializeState(state);\n\n // Determine TTL\n let ttl = this.options.defaultTtlSeconds;\n if (state.metadata.isCompleted && this.options.completedTtlSeconds > 0) {\n ttl = this.options.completedTtlSeconds;\n }\n\n const multi = this.redis.multi();\n\n if (ttl > 0) {\n multi.setex(key, ttl, serialized);\n multi.setex(indexKey, ttl, correlationId);\n } else {\n multi.set(key, serialized);\n multi.set(indexKey, correlationId);\n }\n\n await multi.exec();\n }\n\n async update(\n sagaName: string,\n state: TState,\n expectedVersion: number\n ): Promise<void> {\n if (!this.redis) throw new Error(\"Store not initialized\");\n\n // We need to find the correlation ID for this saga\n const indexKey = this.buildIdIndexKey(sagaName, state.metadata.sagaId);\n const correlationId = await this.redis.get(indexKey);\n\n if (!correlationId) {\n throw new Error(`Saga ${state.metadata.sagaId} not found`);\n }\n\n const key = this.buildKey(sagaName, correlationId);\n\n for (let attempt = 0; attempt < this.options.maxRetries; attempt++) {\n // Watch the key for changes\n await this.redis.watch(key);\n\n try {\n // Check current version\n const existing = await this.redis.get(key);\n if (!existing) {\n await this.redis.unwatch();\n throw new Error(`Saga ${state.metadata.sagaId} not found`);\n }\n\n const currentState = this.deserializeState(existing);\n if (currentState.metadata.version !== expectedVersion) {\n await this.redis.unwatch();\n throw new ConcurrencyError(\n state.metadata.sagaId,\n expectedVersion,\n currentState.metadata.version\n );\n }\n\n // Start transaction\n const multi = this.redis.multi();\n\n // Serialize and save\n const serialized = this.serializeState(state);\n\n // Determine TTL\n let ttl = this.options.defaultTtlSeconds;\n if (state.metadata.isCompleted && this.options.completedTtlSeconds > 0) {\n ttl = this.options.completedTtlSeconds;\n }\n\n if (ttl > 0) {\n multi.setex(key, ttl, serialized);\n multi.setex(indexKey, ttl, correlationId);\n } else {\n multi.set(key, serialized);\n multi.set(indexKey, correlationId);\n }\n\n // Execute transaction\n const result = await multi.exec();\n\n if (result === null) {\n // Transaction aborted due to WATCH - retry\n if (attempt < this.options.maxRetries - 1) {\n await this.delay(this.options.retryDelayMs);\n continue;\n }\n throw new ConcurrencyError(\n state.metadata.sagaId,\n expectedVersion,\n -1 // Unknown current version due to race\n );\n }\n\n return; // Success\n } catch (error) {\n await this.redis.unwatch();\n throw error;\n }\n }\n }\n\n async delete(sagaName: string, sagaId: string): Promise<void> {\n if (!this.redis) throw new Error(\"Store not initialized\");\n\n // Lookup correlation ID from index\n const indexKey = this.buildIdIndexKey(sagaName, sagaId);\n const correlationId = await this.redis.get(indexKey);\n\n if (!correlationId) return;\n\n const key = this.buildKey(sagaName, correlationId);\n await this.redis.del(key, indexKey);\n }\n\n private buildKey(sagaName: string, correlationId: string): string {\n return `${this.options.keyPrefix}saga:${sagaName}:${correlationId}`;\n }\n\n private buildIdIndexKey(sagaName: string, sagaId: string): string {\n return `${this.options.keyPrefix}saga:${sagaName}:idx:id:${sagaId}`;\n }\n\n private serializeState(state: TState): string {\n return JSON.stringify({\n ...state,\n metadata: {\n ...state.metadata,\n createdAt: state.metadata.createdAt.toISOString(),\n updatedAt: state.metadata.updatedAt.toISOString(),\n archivedAt: state.metadata.archivedAt?.toISOString() ?? null,\n timeoutExpiresAt: state.metadata.timeoutExpiresAt?.toISOString() ?? null,\n },\n });\n }\n\n private deserializeState(data: string): TState {\n const parsed = JSON.parse(data);\n return {\n ...parsed,\n metadata: {\n ...parsed.metadata,\n createdAt: new Date(parsed.metadata.createdAt),\n updatedAt: new Date(parsed.metadata.updatedAt),\n archivedAt: parsed.metadata.archivedAt\n ? new Date(parsed.metadata.archivedAt)\n : null,\n timeoutExpiresAt: parsed.metadata.timeoutExpiresAt\n ? new Date(parsed.metadata.timeoutExpiresAt)\n : null,\n },\n } as TState;\n }\n\n private delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n\n // ============ Query Helpers ============\n\n /**\n * Get the underlying Redis client for advanced operations.\n */\n getRedis(): Redis | null {\n return this.redis;\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@saga-bus/store-redis",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Redis saga store for saga-bus",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@saga-bus/core": "0.1.0"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"ioredis": ">=5.0.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^22.10.1",
|
|
28
|
+
"eslint": "^9.16.0",
|
|
29
|
+
"tsup": "^8.3.5",
|
|
30
|
+
"typescript": "^5.7.2",
|
|
31
|
+
"vitest": "^2.1.8",
|
|
32
|
+
"@repo/eslint-config": "0.0.0",
|
|
33
|
+
"@repo/typescript-config": "0.0.0"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup",
|
|
37
|
+
"dev": "tsup --watch",
|
|
38
|
+
"lint": "eslint src/",
|
|
39
|
+
"check-types": "tsc --noEmit",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"test:watch": "vitest"
|
|
42
|
+
}
|
|
43
|
+
}
|