@pikku/redis 0.11.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/CHANGELOG.md +18 -0
- package/README.md +202 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/redis-channel-store.d.ts +18 -0
- package/dist/src/redis-channel-store.js +73 -0
- package/dist/src/redis-channel-store.js.map +1 -0
- package/dist/src/redis-eventhub-store.d.ts +15 -0
- package/dist/src/redis-eventhub-store.js +58 -0
- package/dist/src/redis-eventhub-store.js.map +1 -0
- package/dist/src/redis-workflow-service.d.ts +35 -0
- package/dist/src/redis-workflow-service.js +341 -0
- package/dist/src/redis-workflow-service.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +27 -0
- package/run-tests.sh +33 -0
- package/src/index.ts +3 -0
- package/src/redis-channel-store.ts +136 -0
- package/src/redis-eventhub-store.ts +104 -0
- package/src/redis-workflow-service.ts +678 -0
- package/tsconfig.json +17 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
## 0.11.0
|
|
2
|
+
|
|
3
|
+
### Minor Changes
|
|
4
|
+
|
|
5
|
+
- Initial release with Redis-backed channel, eventhub, and workflow stores
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# @pikku-workflows/redis
|
|
9
|
+
|
|
10
|
+
## 0.10.0
|
|
11
|
+
|
|
12
|
+
### Major Changes
|
|
13
|
+
|
|
14
|
+
- Initial release of @pikku-workflows/redis
|
|
15
|
+
- Redis-based WorkflowStateService implementation
|
|
16
|
+
- Distributed locking with Redis SET NX
|
|
17
|
+
- Support for shared or owned Redis connections
|
|
18
|
+
- Configurable key prefixes
|
package/README.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# @pikku-workflows/redis
|
|
2
|
+
|
|
3
|
+
Redis-based implementation of WorkflowStateService for Pikku Workflows.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **RedisWorkflowService**: Redis implementation of WorkflowStateService for persistent workflow execution state
|
|
8
|
+
- **Fast in-memory storage**: Leverage Redis for high-performance workflow state management
|
|
9
|
+
- **Configurable key prefix**: Use custom key prefixes for namespace isolation (default: 'workflows')
|
|
10
|
+
- **Distributed locking**: Uses Redis SET NX with TTL for concurrent run safety
|
|
11
|
+
- **Type-safe**: Full TypeScript support
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @pikku-workflows/redis ioredis
|
|
17
|
+
# or
|
|
18
|
+
yarn add @pikku-workflows/redis ioredis
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
### Basic Setup (Remote Mode)
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import Redis from 'ioredis'
|
|
27
|
+
import { RedisWorkflowService } from '@pikku-workflows/redis'
|
|
28
|
+
import { BullQueueService } from '@pikku/queue-bullmq'
|
|
29
|
+
|
|
30
|
+
// Create Redis connection
|
|
31
|
+
const redis = new Redis({
|
|
32
|
+
host: 'localhost',
|
|
33
|
+
port: 6379,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// Create queue service for remote mode
|
|
37
|
+
const queueService = new BullQueueService('redis://localhost:6379')
|
|
38
|
+
|
|
39
|
+
// Create workflow state service
|
|
40
|
+
const workflowService = new RedisWorkflowService(
|
|
41
|
+
redis,
|
|
42
|
+
queueService,
|
|
43
|
+
'workflows'
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
// Initialize (verifies connection)
|
|
47
|
+
await workflowService.init()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Inline Mode Setup (Testing)
|
|
51
|
+
|
|
52
|
+
For testing, pass `undefined` as the queue service to enable inline mode:
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// Create workflow state service without queue = inline mode
|
|
56
|
+
const workflowService = new RedisWorkflowService(
|
|
57
|
+
redis,
|
|
58
|
+
undefined, // No queue service = inline mode
|
|
59
|
+
'workflows'
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
await workflowService.init()
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Custom Key Prefix
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// Use a custom key prefix
|
|
69
|
+
const workflowService = new RedisWorkflowService(
|
|
70
|
+
redis,
|
|
71
|
+
queueService,
|
|
72
|
+
'myapp_workflows'
|
|
73
|
+
)
|
|
74
|
+
await workflowService.init()
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### With Connection String
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// Create service with Redis connection string
|
|
81
|
+
const workflowService = new RedisWorkflowService(
|
|
82
|
+
'redis://localhost:6379',
|
|
83
|
+
queueService
|
|
84
|
+
)
|
|
85
|
+
await workflowService.init()
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### With Existing Connection
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// Share connection with other services
|
|
92
|
+
const redis = new Redis('redis://localhost:6379')
|
|
93
|
+
const workflowService = new RedisWorkflowService(redis, queueService)
|
|
94
|
+
// Connection is shared, won't be closed by workflowService.close()
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### With Config (Owned Connection)
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
// Let service create its own connection
|
|
101
|
+
const workflowService = new RedisWorkflowService(
|
|
102
|
+
{ host, port, password },
|
|
103
|
+
queueService
|
|
104
|
+
)
|
|
105
|
+
await workflowService.init()
|
|
106
|
+
// Later...
|
|
107
|
+
await workflowService.close() // Closes the connection
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Redis Data Structure
|
|
111
|
+
|
|
112
|
+
The service uses the following Redis data structures:
|
|
113
|
+
|
|
114
|
+
### Workflow Runs
|
|
115
|
+
|
|
116
|
+
- **Hash**: `{keyPrefix}:run:{runId}` - Stores workflow run data
|
|
117
|
+
- `id`: Run ID
|
|
118
|
+
- `workflow`: Workflow name
|
|
119
|
+
- `status`: Current status ('running', 'completed', 'failed', 'cancelled')
|
|
120
|
+
- `input`: JSON string of input data
|
|
121
|
+
- `output`: JSON string of output data (if completed)
|
|
122
|
+
- `error`: JSON string of error (if failed)
|
|
123
|
+
- `createdAt`: Timestamp
|
|
124
|
+
- `updatedAt`: Timestamp
|
|
125
|
+
|
|
126
|
+
### Workflow Steps
|
|
127
|
+
|
|
128
|
+
- **Hash**: `{keyPrefix}:step:{runId}:{stepName}` - Stores step execution data
|
|
129
|
+
- `stepId`: Unique step ID
|
|
130
|
+
- `status`: Step status ('pending', 'scheduled', 'succeeded', 'failed')
|
|
131
|
+
- `rpcName`: RPC function name (if RPC step)
|
|
132
|
+
- `data`: JSON string of step input data
|
|
133
|
+
- `result`: JSON string of result (if succeeded)
|
|
134
|
+
- `error`: JSON string of error (if failed)
|
|
135
|
+
- `retries`: Number of retry attempts allowed
|
|
136
|
+
- `retryDelay`: Delay between retries
|
|
137
|
+
- `attemptCount`: Current attempt number
|
|
138
|
+
- `createdAt`: Timestamp
|
|
139
|
+
- `updatedAt`: Timestamp
|
|
140
|
+
|
|
141
|
+
### Workflow Step History
|
|
142
|
+
|
|
143
|
+
- **List**: `{keyPrefix}:history:{runId}` - Stores all step attempts in chronological order
|
|
144
|
+
- Each entry contains complete step state for that attempt
|
|
145
|
+
|
|
146
|
+
### Locking
|
|
147
|
+
|
|
148
|
+
- **String**: `{keyPrefix}:lock:{runId}` - Distributed lock with TTL (30 seconds)
|
|
149
|
+
|
|
150
|
+
## API
|
|
151
|
+
|
|
152
|
+
### RedisWorkflowService
|
|
153
|
+
|
|
154
|
+
Extends `WorkflowStateService` from `@pikku/core/workflow`.
|
|
155
|
+
|
|
156
|
+
#### Constructor
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
new RedisWorkflowService(
|
|
160
|
+
connectionOrConfig: Redis | RedisOptions | string,
|
|
161
|
+
queue?: any,
|
|
162
|
+
keyPrefix?: string
|
|
163
|
+
)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
- `connectionOrConfig`: ioredis Redis instance, RedisOptions config, or connection string
|
|
167
|
+
- `queue`: Optional queue service for remote workflow execution
|
|
168
|
+
- `keyPrefix`: Redis key prefix (default: 'workflows')
|
|
169
|
+
|
|
170
|
+
#### Methods
|
|
171
|
+
|
|
172
|
+
- `init()`: Initialize the service (verifies Redis connection)
|
|
173
|
+
- `createRun(workflowName, input)`: Create a new workflow run
|
|
174
|
+
- `getRun(id)`: Get workflow run by ID
|
|
175
|
+
- `getRunHistory(runId)`: Get all step attempts in chronological order
|
|
176
|
+
- `updateRunStatus(id, status, output?, error?)`: Update run status
|
|
177
|
+
- `insertStepState(runId, stepName, rpcName, data, stepOptions?)`: Insert initial step state
|
|
178
|
+
- `getStepState(runId, stepName)`: Get step state with attempt count
|
|
179
|
+
- `setStepScheduled(stepId)`: Mark step as scheduled
|
|
180
|
+
- `setStepRunning(stepId)`: Mark step as running
|
|
181
|
+
- `setStepResult(stepId, result)`: Store step result and mark as succeeded
|
|
182
|
+
- `setStepError(stepId, error)`: Store step error and mark as failed
|
|
183
|
+
- `createRetryAttempt(failedStepId)`: Create a new retry attempt for a failed step
|
|
184
|
+
- `withRunLock(id, fn)`: Execute function with distributed lock
|
|
185
|
+
- `close()`: Close Redis connection (if owned)
|
|
186
|
+
|
|
187
|
+
## Documentation
|
|
188
|
+
|
|
189
|
+
For complete workflow documentation, see [pikku.dev/docs/workflows](https://pikku.dev/docs/workflows)
|
|
190
|
+
|
|
191
|
+
## Locking Behavior
|
|
192
|
+
|
|
193
|
+
The `withRunLock` method uses Redis SET NX with a 30-second TTL for distributed locking:
|
|
194
|
+
|
|
195
|
+
- Retries up to 10 times with 100ms delay between attempts
|
|
196
|
+
- Automatically releases lock after function execution
|
|
197
|
+
- Uses Lua script to ensure only the lock owner can release it
|
|
198
|
+
- Lock automatically expires after 30 seconds if not released
|
|
199
|
+
|
|
200
|
+
## License
|
|
201
|
+
|
|
202
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAC9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAA"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { CoreUserSession } from '@pikku/core';
|
|
2
|
+
import { Channel, ChannelStore } from '@pikku/core/channel';
|
|
3
|
+
import { Redis, type RedisOptions } from 'ioredis';
|
|
4
|
+
export declare class RedisChannelStore extends ChannelStore {
|
|
5
|
+
private redis;
|
|
6
|
+
private keyPrefix;
|
|
7
|
+
private ownsConnection;
|
|
8
|
+
constructor(connectionOrConfig: Redis | RedisOptions | string, keyPrefix?: string);
|
|
9
|
+
init(): Promise<void>;
|
|
10
|
+
private channelKey;
|
|
11
|
+
addChannel({ channelId, channelName, openingData, }: Channel): Promise<void>;
|
|
12
|
+
removeChannels(channelIds: string[]): Promise<void>;
|
|
13
|
+
setUserSession(channelId: string, session: CoreUserSession | null): Promise<void>;
|
|
14
|
+
getChannelAndSession(channelId: string): Promise<Channel & {
|
|
15
|
+
session: CoreUserSession;
|
|
16
|
+
}>;
|
|
17
|
+
close(): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { ChannelStore } from '@pikku/core/channel';
|
|
2
|
+
import { Redis } from 'ioredis';
|
|
3
|
+
export class RedisChannelStore extends ChannelStore {
|
|
4
|
+
redis;
|
|
5
|
+
keyPrefix;
|
|
6
|
+
ownsConnection;
|
|
7
|
+
constructor(connectionOrConfig, keyPrefix = 'pikku') {
|
|
8
|
+
super();
|
|
9
|
+
this.keyPrefix = keyPrefix;
|
|
10
|
+
if (connectionOrConfig instanceof Redis) {
|
|
11
|
+
this.redis = connectionOrConfig;
|
|
12
|
+
this.ownsConnection = false;
|
|
13
|
+
}
|
|
14
|
+
else if (typeof connectionOrConfig === 'string') {
|
|
15
|
+
this.redis = new Redis(connectionOrConfig);
|
|
16
|
+
this.ownsConnection = true;
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
this.redis = new Redis(connectionOrConfig);
|
|
20
|
+
this.ownsConnection = true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async init() {
|
|
24
|
+
await this.redis.ping();
|
|
25
|
+
}
|
|
26
|
+
channelKey(channelId) {
|
|
27
|
+
return `${this.keyPrefix}:channel:${channelId}`;
|
|
28
|
+
}
|
|
29
|
+
async addChannel({ channelId, channelName, openingData, }) {
|
|
30
|
+
const key = this.channelKey(channelId);
|
|
31
|
+
await this.redis.hmset(key, 'channelId', channelId, 'channelName', channelName, 'openingData', JSON.stringify(openingData || {}), 'createdAt', Date.now().toString());
|
|
32
|
+
}
|
|
33
|
+
async removeChannels(channelIds) {
|
|
34
|
+
if (channelIds.length === 0) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const pipeline = this.redis.pipeline();
|
|
38
|
+
for (const channelId of channelIds) {
|
|
39
|
+
const key = this.channelKey(channelId);
|
|
40
|
+
pipeline.del(key);
|
|
41
|
+
pipeline.del(`${this.keyPrefix}:subs:${channelId}`);
|
|
42
|
+
}
|
|
43
|
+
await pipeline.exec();
|
|
44
|
+
}
|
|
45
|
+
async setUserSession(channelId, session) {
|
|
46
|
+
const key = this.channelKey(channelId);
|
|
47
|
+
if (session) {
|
|
48
|
+
await this.redis.hset(key, 'userSession', JSON.stringify(session));
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
await this.redis.hdel(key, 'userSession');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async getChannelAndSession(channelId) {
|
|
55
|
+
const key = this.channelKey(channelId);
|
|
56
|
+
const data = await this.redis.hgetall(key);
|
|
57
|
+
if (!data.channelId) {
|
|
58
|
+
throw new Error(`Channel not found: ${channelId}`);
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
channelId: data.channelId,
|
|
62
|
+
channelName: data.channelName,
|
|
63
|
+
openingData: data.openingData ? JSON.parse(data.openingData) : {},
|
|
64
|
+
session: data.userSession ? JSON.parse(data.userSession) : {},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
async close() {
|
|
68
|
+
if (this.ownsConnection) {
|
|
69
|
+
await this.redis.quit();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=redis-channel-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-channel-store.js","sourceRoot":"","sources":["../../src/redis-channel-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { EventHubStore } from '@pikku/core/channel';
|
|
2
|
+
import { Redis, type RedisOptions } from 'ioredis';
|
|
3
|
+
export declare class RedisEventHubStore implements EventHubStore {
|
|
4
|
+
private redis;
|
|
5
|
+
private keyPrefix;
|
|
6
|
+
private ownsConnection;
|
|
7
|
+
constructor(connectionOrConfig: Redis | RedisOptions | string, keyPrefix?: string);
|
|
8
|
+
init(): Promise<void>;
|
|
9
|
+
private topicKey;
|
|
10
|
+
private channelSubsKey;
|
|
11
|
+
getChannelIdsForTopic(topic: string): Promise<string[]>;
|
|
12
|
+
subscribe(topic: string, channelId: string): Promise<boolean>;
|
|
13
|
+
unsubscribe(topic: string, channelId: string): Promise<boolean>;
|
|
14
|
+
close(): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Redis } from 'ioredis';
|
|
2
|
+
export class RedisEventHubStore {
|
|
3
|
+
redis;
|
|
4
|
+
keyPrefix;
|
|
5
|
+
ownsConnection;
|
|
6
|
+
constructor(connectionOrConfig, keyPrefix = 'pikku') {
|
|
7
|
+
this.keyPrefix = keyPrefix;
|
|
8
|
+
if (connectionOrConfig instanceof Redis) {
|
|
9
|
+
this.redis = connectionOrConfig;
|
|
10
|
+
this.ownsConnection = false;
|
|
11
|
+
}
|
|
12
|
+
else if (typeof connectionOrConfig === 'string') {
|
|
13
|
+
this.redis = new Redis(connectionOrConfig);
|
|
14
|
+
this.ownsConnection = true;
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
this.redis = new Redis(connectionOrConfig);
|
|
18
|
+
this.ownsConnection = true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async init() {
|
|
22
|
+
await this.redis.ping();
|
|
23
|
+
}
|
|
24
|
+
topicKey(topic) {
|
|
25
|
+
return `${this.keyPrefix}:topic:${topic}`;
|
|
26
|
+
}
|
|
27
|
+
channelSubsKey(channelId) {
|
|
28
|
+
return `${this.keyPrefix}:subs:${channelId}`;
|
|
29
|
+
}
|
|
30
|
+
async getChannelIdsForTopic(topic) {
|
|
31
|
+
const key = this.topicKey(topic);
|
|
32
|
+
return await this.redis.smembers(key);
|
|
33
|
+
}
|
|
34
|
+
async subscribe(topic, channelId) {
|
|
35
|
+
const topicKey = this.topicKey(topic);
|
|
36
|
+
const subsKey = this.channelSubsKey(channelId);
|
|
37
|
+
const pipeline = this.redis.pipeline();
|
|
38
|
+
pipeline.sadd(topicKey, channelId);
|
|
39
|
+
pipeline.sadd(subsKey, topic);
|
|
40
|
+
await pipeline.exec();
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
async unsubscribe(topic, channelId) {
|
|
44
|
+
const topicKey = this.topicKey(topic);
|
|
45
|
+
const subsKey = this.channelSubsKey(channelId);
|
|
46
|
+
const pipeline = this.redis.pipeline();
|
|
47
|
+
pipeline.srem(topicKey, channelId);
|
|
48
|
+
pipeline.srem(subsKey, topic);
|
|
49
|
+
const results = await pipeline.exec();
|
|
50
|
+
return results[0][1] > 0;
|
|
51
|
+
}
|
|
52
|
+
async close() {
|
|
53
|
+
if (this.ownsConnection) {
|
|
54
|
+
await this.redis.quit();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=redis-eventhub-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-eventhub-store.js","sourceRoot":"","sources":["../../src/redis-eventhub-store.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAqB,MAAM,SAAS,CAAA;AAclD,MAAM,OAAO,kBAAkB;IACrB,KAAK,CAAO;IACZ,SAAS,CAAQ;IACjB,cAAc,CAAS;IAM/B,YACE,kBAAiD,EACjD,SAAS,GAAG,OAAO;QAEnB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAA;QAG1B,IAAI,kBAAkB,YAAY,KAAK,EAAE,CAAC;YACxC,IAAI,CAAC,KAAK,GAAG,kBAAkB,CAAA;YAC/B,IAAI,CAAC,cAAc,GAAG,KAAK,CAAA;QAC7B,CAAC;aAAM,IAAI,OAAO,kBAAkB,KAAK,QAAQ,EAAE,CAAC;YAElD,IAAI,CAAC,KAAK,GAAG,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAA;YAC1C,IAAI,CAAC,cAAc,GAAG,IAAI,CAAA;QAC5B,CAAC;aAAM,CAAC;YAEN,IAAI,CAAC,KAAK,GAAG,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAA;YAC1C,IAAI,CAAC,cAAc,GAAG,IAAI,CAAA;QAC5B,CAAC;IACH,CAAC;IAKM,KAAK,CAAC,IAAI;QAEf,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAA;IACzB,CAAC;IAEO,QAAQ,CAAC,KAAa;QAC5B,OAAO,GAAG,IAAI,CAAC,SAAS,UAAU,KAAK,EAAE,CAAA;IAC3C,CAAC;IAEO,cAAc,CAAC,SAAiB;QACtC,OAAO,GAAG,IAAI,CAAC,SAAS,SAAS,SAAS,EAAE,CAAA;IAC9C,CAAC;IAEM,KAAK,CAAC,qBAAqB,CAAC,KAAa;QAC9C,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;QAChC,OAAO,MAAM,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAA;IACvC,CAAC;IAEM,KAAK,CAAC,SAAS,CAAC,KAAa,EAAE,SAAiB;QACrD,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;QACrC,MAAM,OAAO,GAAG,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC,CAAA;QAG9C,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAA;QACtC,QAAQ,CAAC,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAA;QAClC,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAA;QAE7B,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;QAErB,OAAO,IAAI,CAAA;IACb,CAAC;IAEM,KAAK,CAAC,WAAW,CAAC,KAAa,EAAE,SAAiB;QACvD,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;QACrC,MAAM,OAAO,GAAG,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC,CAAA;QAG9C,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAA;QACtC,QAAQ,CAAC,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAA;QAClC,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAA;QAE7B,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;QAGrC,OAAQ,OAAQ,CAAC,CAAC,CAAE,CAAC,CAAC,CAAY,GAAG,CAAC,CAAA;IACxC,CAAC;IAKM,KAAK,CAAC,KAAK;QAChB,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACxB,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAA;QACzB,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { SerializedError } from '@pikku/core';
|
|
2
|
+
import { PikkuWorkflowService, type WorkflowRun, type StepState, type WorkflowStatus } from '@pikku/core/workflow';
|
|
3
|
+
import { Redis, type RedisOptions } from 'ioredis';
|
|
4
|
+
export declare class RedisWorkflowService extends PikkuWorkflowService {
|
|
5
|
+
private redis;
|
|
6
|
+
private keyPrefix;
|
|
7
|
+
private ownsConnection;
|
|
8
|
+
private lockTTL;
|
|
9
|
+
constructor(connectionOrConfig: Redis | RedisOptions | string | undefined, keyPrefix?: string);
|
|
10
|
+
init(): Promise<void>;
|
|
11
|
+
private runKey;
|
|
12
|
+
private stepKey;
|
|
13
|
+
private stepHistoryKey;
|
|
14
|
+
private lockKey;
|
|
15
|
+
private saveStepHistory;
|
|
16
|
+
private updateCurrentHistoryRecord;
|
|
17
|
+
createRun(workflowName: string, input: any): Promise<string>;
|
|
18
|
+
getRun(id: string): Promise<WorkflowRun | null>;
|
|
19
|
+
updateRunStatus(id: string, status: WorkflowStatus, output?: any, error?: SerializedError): Promise<void>;
|
|
20
|
+
insertStepState(runId: string, stepName: string, rpcName: string, data: any, stepOptions?: {
|
|
21
|
+
retries?: number;
|
|
22
|
+
retryDelay?: string | number;
|
|
23
|
+
}): Promise<StepState>;
|
|
24
|
+
getStepState(runId: string, stepName: string): Promise<StepState>;
|
|
25
|
+
getRunHistory(runId: string): Promise<Array<StepState & {
|
|
26
|
+
stepName: string;
|
|
27
|
+
}>>;
|
|
28
|
+
setStepRunning(stepId: string): Promise<void>;
|
|
29
|
+
setStepScheduled(stepId: string): Promise<void>;
|
|
30
|
+
setStepResult(stepId: string, result: any): Promise<void>;
|
|
31
|
+
setStepError(stepId: string, error: Error): Promise<void>;
|
|
32
|
+
withRunLock<T>(id: string, fn: () => Promise<T>): Promise<T>;
|
|
33
|
+
createRetryAttempt(stepId: string, status: 'pending' | 'running'): Promise<StepState>;
|
|
34
|
+
close(): Promise<void>;
|
|
35
|
+
}
|