@pikku/redis 0.11.2 → 0.12.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 +14 -1
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/redis-agent-run-service.d.ts +24 -0
- package/dist/src/redis-agent-run-service.js +141 -0
- package/dist/src/redis-agent-run-service.js.map +1 -0
- package/dist/src/redis-deployment-service.d.ts +19 -0
- package/dist/src/redis-deployment-service.js +111 -0
- package/dist/src/redis-deployment-service.js.map +1 -0
- package/dist/src/redis-workflow-run-service.d.ts +35 -0
- package/dist/src/redis-workflow-run-service.js +167 -0
- package/dist/src/redis-workflow-run-service.js.map +1 -0
- package/dist/src/redis-workflow-service.d.ts +9 -2
- package/dist/src/redis-workflow-service.js +48 -16
- package/dist/src/redis-workflow-service.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/index.ts +5 -0
- package/src/redis-agent-run-service.ts +181 -0
- package/src/redis-deployment-service.ts +153 -0
- package/src/redis-workflow-run-service.ts +227 -0
- package/src/redis-workflow-service.ts +77 -19
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
WorkflowRun,
|
|
3
|
+
StepState,
|
|
4
|
+
WorkflowStatus,
|
|
5
|
+
WorkflowRunService,
|
|
6
|
+
} from '@pikku/core/workflow'
|
|
7
|
+
import type { Redis } from 'ioredis'
|
|
8
|
+
|
|
9
|
+
export class RedisWorkflowRunService implements WorkflowRunService {
|
|
10
|
+
constructor(
|
|
11
|
+
private redis: Redis,
|
|
12
|
+
private keyPrefix = 'workflows'
|
|
13
|
+
) {}
|
|
14
|
+
|
|
15
|
+
private runKey(runId: string): string {
|
|
16
|
+
return `${this.keyPrefix}:run:${runId}`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private stepKeyPattern(runId: string): string {
|
|
20
|
+
return `${this.keyPrefix}:step:${runId}:*`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private stepHistoryKey(stepId: string): string {
|
|
24
|
+
return `${this.keyPrefix}:step-history:${stepId}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private versionKey(name: string, graphHash: string): string {
|
|
28
|
+
return `${this.keyPrefix}:version:${name}:${graphHash}`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async listRuns(options?: {
|
|
32
|
+
workflowName?: string
|
|
33
|
+
status?: string
|
|
34
|
+
limit?: number
|
|
35
|
+
offset?: number
|
|
36
|
+
}): Promise<WorkflowRun[]> {
|
|
37
|
+
const { workflowName, status, limit = 50, offset = 0 } = options ?? {}
|
|
38
|
+
|
|
39
|
+
const runKeys = await this.scanKeys(`${this.keyPrefix}:run:*`)
|
|
40
|
+
|
|
41
|
+
const runs: WorkflowRun[] = []
|
|
42
|
+
for (const key of runKeys) {
|
|
43
|
+
const data = await this.redis.hgetall(key)
|
|
44
|
+
if (!data.id) continue
|
|
45
|
+
|
|
46
|
+
if (workflowName && data.workflow !== workflowName) continue
|
|
47
|
+
if (status && data.status !== status) continue
|
|
48
|
+
|
|
49
|
+
runs.push(this.mapRunData(data))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
runs.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
|
53
|
+
|
|
54
|
+
return runs.slice(offset, offset + limit)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async getRun(id: string): Promise<WorkflowRun | null> {
|
|
58
|
+
const data = await this.redis.hgetall(this.runKey(id))
|
|
59
|
+
if (!data.id) return null
|
|
60
|
+
return this.mapRunData(data)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async getRunSteps(
|
|
64
|
+
runId: string
|
|
65
|
+
): Promise<
|
|
66
|
+
Array<StepState & { stepName: string; rpcName?: string; data?: any }>
|
|
67
|
+
> {
|
|
68
|
+
const stepKeys = await this.scanKeys(this.stepKeyPattern(runId))
|
|
69
|
+
|
|
70
|
+
const steps: Array<
|
|
71
|
+
StepState & { stepName: string; rpcName?: string; data?: any }
|
|
72
|
+
> = []
|
|
73
|
+
|
|
74
|
+
for (const key of stepKeys) {
|
|
75
|
+
const data = await this.redis.hgetall(key)
|
|
76
|
+
if (!data.stepId) continue
|
|
77
|
+
|
|
78
|
+
steps.push({
|
|
79
|
+
stepId: data.stepId,
|
|
80
|
+
stepName: this.extractStepName(key, runId),
|
|
81
|
+
rpcName: data.rpcName || undefined,
|
|
82
|
+
data: data.data ? JSON.parse(data.data) : undefined,
|
|
83
|
+
status: data.status as any,
|
|
84
|
+
result: data.result ? JSON.parse(data.result) : undefined,
|
|
85
|
+
error: data.error ? JSON.parse(data.error) : undefined,
|
|
86
|
+
attemptCount: Number(data.attemptCount || 1),
|
|
87
|
+
retries: data.retries ? Number(data.retries) : undefined,
|
|
88
|
+
retryDelay: data.retryDelay || undefined,
|
|
89
|
+
createdAt: new Date(Number(data.createdAt)),
|
|
90
|
+
updatedAt: new Date(Number(data.updatedAt)),
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
steps.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())
|
|
95
|
+
|
|
96
|
+
return steps
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async getRunHistory(
|
|
100
|
+
runId: string
|
|
101
|
+
): Promise<Array<StepState & { stepName: string }>> {
|
|
102
|
+
const stepKeys = await this.scanKeys(this.stepKeyPattern(runId))
|
|
103
|
+
|
|
104
|
+
const allHistoryEntries: Array<StepState & { stepName: string }> = []
|
|
105
|
+
|
|
106
|
+
for (const key of stepKeys) {
|
|
107
|
+
const data = await this.redis.hgetall(key)
|
|
108
|
+
if (!data.stepId) continue
|
|
109
|
+
|
|
110
|
+
const historyEntries = await this.redis.zrange(
|
|
111
|
+
this.stepHistoryKey(data.stepId),
|
|
112
|
+
0,
|
|
113
|
+
-1
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
for (const entryStr of historyEntries) {
|
|
117
|
+
const entry = JSON.parse(entryStr)
|
|
118
|
+
allHistoryEntries.push({
|
|
119
|
+
stepId: entry.stepId,
|
|
120
|
+
stepName: entry.stepName,
|
|
121
|
+
status: entry.status,
|
|
122
|
+
result: entry.result ? JSON.parse(entry.result) : undefined,
|
|
123
|
+
error: entry.error ? JSON.parse(entry.error) : undefined,
|
|
124
|
+
attemptCount: entry.attemptCount,
|
|
125
|
+
retries: entry.retries,
|
|
126
|
+
retryDelay: entry.retryDelay,
|
|
127
|
+
createdAt: new Date(entry.createdAt),
|
|
128
|
+
updatedAt: new Date(entry.createdAt),
|
|
129
|
+
runningAt: entry.runningAt ? new Date(entry.runningAt) : undefined,
|
|
130
|
+
scheduledAt: entry.scheduledAt
|
|
131
|
+
? new Date(entry.scheduledAt)
|
|
132
|
+
: undefined,
|
|
133
|
+
succeededAt: entry.succeededAt
|
|
134
|
+
? new Date(entry.succeededAt)
|
|
135
|
+
: undefined,
|
|
136
|
+
failedAt: entry.failedAt ? new Date(entry.failedAt) : undefined,
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return allHistoryEntries.sort(
|
|
142
|
+
(a, b) => a.createdAt.getTime() - b.createdAt.getTime()
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async getDistinctWorkflowNames(): Promise<string[]> {
|
|
147
|
+
const runKeys = await this.scanKeys(`${this.keyPrefix}:run:*`)
|
|
148
|
+
|
|
149
|
+
const names = new Set<string>()
|
|
150
|
+
for (const key of runKeys) {
|
|
151
|
+
const workflow = await this.redis.hget(key, 'workflow')
|
|
152
|
+
if (workflow) names.add(workflow)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return [...names].sort()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async getWorkflowVersion(
|
|
159
|
+
name: string,
|
|
160
|
+
graphHash: string
|
|
161
|
+
): Promise<{ graph: any; source: string } | null> {
|
|
162
|
+
const data = await this.redis.hgetall(this.versionKey(name, graphHash))
|
|
163
|
+
if (!data.graph) return null
|
|
164
|
+
return {
|
|
165
|
+
graph: JSON.parse(data.graph),
|
|
166
|
+
source: data.source!,
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async deleteRun(id: string): Promise<boolean> {
|
|
171
|
+
const exists = await this.redis.exists(this.runKey(id))
|
|
172
|
+
if (!exists) return false
|
|
173
|
+
|
|
174
|
+
const stepKeys = await this.scanKeys(this.stepKeyPattern(id))
|
|
175
|
+
|
|
176
|
+
const keysToDelete = [this.runKey(id)]
|
|
177
|
+
|
|
178
|
+
for (const stepKey of stepKeys) {
|
|
179
|
+
const stepId = await this.redis.hget(stepKey, 'stepId')
|
|
180
|
+
keysToDelete.push(stepKey)
|
|
181
|
+
if (stepId) {
|
|
182
|
+
keysToDelete.push(this.stepHistoryKey(stepId))
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
await this.redis.del(...keysToDelete)
|
|
187
|
+
return true
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private mapRunData(data: Record<string, string>): WorkflowRun {
|
|
191
|
+
return {
|
|
192
|
+
id: data.id!,
|
|
193
|
+
workflow: data.workflow!,
|
|
194
|
+
status: data.status! as WorkflowStatus,
|
|
195
|
+
input: JSON.parse(data.input!),
|
|
196
|
+
output: data.output ? JSON.parse(data.output) : undefined,
|
|
197
|
+
error: data.error ? JSON.parse(data.error) : undefined,
|
|
198
|
+
inline: data.inline === 'true' ? true : undefined,
|
|
199
|
+
graphHash: data.graphHash || undefined,
|
|
200
|
+
wire: data.wire ? JSON.parse(data.wire) : { type: 'unknown' },
|
|
201
|
+
createdAt: new Date(Number(data.createdAt!)),
|
|
202
|
+
updatedAt: new Date(Number(data.updatedAt!)),
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private extractStepName(key: string, runId: string): string {
|
|
207
|
+
const prefix = `${this.keyPrefix}:step:${runId}:`
|
|
208
|
+
return key.slice(prefix.length)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private async scanKeys(pattern: string): Promise<string[]> {
|
|
212
|
+
const keys: string[] = []
|
|
213
|
+
let cursor = '0'
|
|
214
|
+
do {
|
|
215
|
+
const [newCursor, foundKeys] = await this.redis.scan(
|
|
216
|
+
cursor,
|
|
217
|
+
'MATCH',
|
|
218
|
+
pattern,
|
|
219
|
+
'COUNT',
|
|
220
|
+
100
|
|
221
|
+
)
|
|
222
|
+
cursor = newCursor
|
|
223
|
+
keys.push(...foundKeys)
|
|
224
|
+
} while (cursor !== '0')
|
|
225
|
+
return keys
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { SerializedError } from '@pikku/core'
|
|
|
2
2
|
import {
|
|
3
3
|
PikkuWorkflowService,
|
|
4
4
|
type WorkflowRun,
|
|
5
|
+
type WorkflowRunWire,
|
|
5
6
|
type StepState,
|
|
6
7
|
type WorkflowStatus,
|
|
7
8
|
} from '@pikku/core/workflow'
|
|
@@ -204,7 +205,9 @@ export class RedisWorkflowService extends PikkuWorkflowService {
|
|
|
204
205
|
async createRun(
|
|
205
206
|
workflowName: string,
|
|
206
207
|
input: any,
|
|
207
|
-
inline
|
|
208
|
+
inline: boolean,
|
|
209
|
+
graphHash: string,
|
|
210
|
+
wire: WorkflowRunWire
|
|
208
211
|
): Promise<string> {
|
|
209
212
|
const id = randomUUID()
|
|
210
213
|
const now = Date.now()
|
|
@@ -223,6 +226,10 @@ export class RedisWorkflowService extends PikkuWorkflowService {
|
|
|
223
226
|
JSON.stringify(input),
|
|
224
227
|
'inline',
|
|
225
228
|
inline ? 'true' : 'false',
|
|
229
|
+
'graphHash',
|
|
230
|
+
graphHash,
|
|
231
|
+
'wire',
|
|
232
|
+
JSON.stringify(wire),
|
|
226
233
|
'createdAt',
|
|
227
234
|
now.toString(),
|
|
228
235
|
'updatedAt',
|
|
@@ -248,6 +255,8 @@ export class RedisWorkflowService extends PikkuWorkflowService {
|
|
|
248
255
|
output: data.output ? JSON.parse(data.output) : undefined,
|
|
249
256
|
error: data.error ? JSON.parse(data.error) : undefined,
|
|
250
257
|
inline: data.inline === 'true' ? true : undefined,
|
|
258
|
+
graphHash: data.graphHash || undefined,
|
|
259
|
+
wire: data.wire ? JSON.parse(data.wire) : { type: 'unknown' },
|
|
251
260
|
createdAt: new Date(Number(data.createdAt!)),
|
|
252
261
|
updatedAt: new Date(Number(data.updatedAt!)),
|
|
253
262
|
}
|
|
@@ -708,13 +717,14 @@ export class RedisWorkflowService extends PikkuWorkflowService {
|
|
|
708
717
|
|
|
709
718
|
async getCompletedGraphState(runId: string): Promise<{
|
|
710
719
|
completedNodeIds: string[]
|
|
720
|
+
failedNodeIds: string[]
|
|
711
721
|
branchKeys: Record<string, string>
|
|
712
722
|
}> {
|
|
713
723
|
const completedNodeIds: string[] = []
|
|
724
|
+
const failedNodeIds: string[] = []
|
|
714
725
|
const branchKeys: Record<string, string> = {}
|
|
715
726
|
|
|
716
|
-
|
|
717
|
-
const pattern = `${this.keyPrefix}:step:${runId}:node:*`
|
|
727
|
+
const pattern = `${this.keyPrefix}:step:${runId}:*`
|
|
718
728
|
let cursor = '0'
|
|
719
729
|
|
|
720
730
|
do {
|
|
@@ -727,28 +737,38 @@ export class RedisWorkflowService extends PikkuWorkflowService {
|
|
|
727
737
|
)
|
|
728
738
|
cursor = newCursor
|
|
729
739
|
|
|
730
|
-
// Check each key for succeeded status and branch_taken
|
|
731
740
|
for (const key of foundKeys) {
|
|
732
|
-
const data = await this.redis.hmget(
|
|
733
|
-
|
|
741
|
+
const data = await this.redis.hmget(
|
|
742
|
+
key,
|
|
743
|
+
'status',
|
|
744
|
+
'branchTaken',
|
|
745
|
+
'attemptCount',
|
|
746
|
+
'retries'
|
|
747
|
+
)
|
|
748
|
+
const [status, branchTaken, attemptCount, retries] = data
|
|
749
|
+
|
|
750
|
+
const parts = key.split(':')
|
|
751
|
+
const stepIndex = parts.lastIndexOf('step')
|
|
752
|
+
if (stepIndex === -1 || stepIndex + 1 >= parts.length) continue
|
|
753
|
+
if (parts[stepIndex + 1] !== runId) continue
|
|
754
|
+
const nodeId = parts.slice(stepIndex + 2).join(':')
|
|
734
755
|
|
|
735
756
|
if (status === 'succeeded') {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
}
|
|
757
|
+
completedNodeIds.push(nodeId)
|
|
758
|
+
if (branchTaken) {
|
|
759
|
+
branchKeys[nodeId] = branchTaken
|
|
760
|
+
}
|
|
761
|
+
} else if (status === 'failed') {
|
|
762
|
+
const maxAttempts = (parseInt(retries || '0', 10) || 0) + 1
|
|
763
|
+
const attempts = parseInt(attemptCount || '0', 10) || 0
|
|
764
|
+
if (attempts >= maxAttempts) {
|
|
765
|
+
failedNodeIds.push(nodeId)
|
|
746
766
|
}
|
|
747
767
|
}
|
|
748
768
|
}
|
|
749
769
|
} while (cursor !== '0')
|
|
750
770
|
|
|
751
|
-
return { completedNodeIds, branchKeys }
|
|
771
|
+
return { completedNodeIds, failedNodeIds, branchKeys }
|
|
752
772
|
}
|
|
753
773
|
|
|
754
774
|
async getNodesWithoutSteps(
|
|
@@ -760,7 +780,7 @@ export class RedisWorkflowService extends PikkuWorkflowService {
|
|
|
760
780
|
const result: string[] = []
|
|
761
781
|
|
|
762
782
|
for (const nodeId of nodeIds) {
|
|
763
|
-
const key = this.stepKey(runId,
|
|
783
|
+
const key = this.stepKey(runId, nodeId)
|
|
764
784
|
const exists = await this.redis.exists(key)
|
|
765
785
|
if (!exists) {
|
|
766
786
|
result.push(nodeId)
|
|
@@ -779,7 +799,7 @@ export class RedisWorkflowService extends PikkuWorkflowService {
|
|
|
779
799
|
const results: Record<string, any> = {}
|
|
780
800
|
|
|
781
801
|
for (const nodeId of nodeIds) {
|
|
782
|
-
const key = this.stepKey(runId,
|
|
802
|
+
const key = this.stepKey(runId, nodeId)
|
|
783
803
|
const data = await this.redis.hmget(key, 'status', 'result')
|
|
784
804
|
const [status, result] = data
|
|
785
805
|
|
|
@@ -842,6 +862,44 @@ export class RedisWorkflowService extends PikkuWorkflowService {
|
|
|
842
862
|
return JSON.parse(stateStr)
|
|
843
863
|
}
|
|
844
864
|
|
|
865
|
+
private versionKey(name: string, graphHash: string): string {
|
|
866
|
+
return `${this.keyPrefix}:version:${name}:${graphHash}`
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
async upsertWorkflowVersion(
|
|
870
|
+
name: string,
|
|
871
|
+
graphHash: string,
|
|
872
|
+
graph: any,
|
|
873
|
+
source: string
|
|
874
|
+
): Promise<void> {
|
|
875
|
+
const key = this.versionKey(name, graphHash)
|
|
876
|
+
const exists = await this.redis.exists(key)
|
|
877
|
+
if (!exists) {
|
|
878
|
+
await this.redis.hmset(
|
|
879
|
+
key,
|
|
880
|
+
'graph',
|
|
881
|
+
JSON.stringify(graph),
|
|
882
|
+
'source',
|
|
883
|
+
source,
|
|
884
|
+
'createdAt',
|
|
885
|
+
Date.now().toString()
|
|
886
|
+
)
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
async getWorkflowVersion(
|
|
891
|
+
name: string,
|
|
892
|
+
graphHash: string
|
|
893
|
+
): Promise<{ graph: any; source: string } | null> {
|
|
894
|
+
const key = this.versionKey(name, graphHash)
|
|
895
|
+
const data = await this.redis.hgetall(key)
|
|
896
|
+
if (!data.graph) return null
|
|
897
|
+
return {
|
|
898
|
+
graph: JSON.parse(data.graph),
|
|
899
|
+
source: data.source!,
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
845
903
|
async close(): Promise<void> {
|
|
846
904
|
if (this.ownsConnection) {
|
|
847
905
|
await this.redis.quit()
|