@pikku/redis 0.11.1 → 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 +31 -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 +18 -2
- package/dist/src/redis-workflow-service.js +112 -2
- 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 +206 -1
|
@@ -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'
|
|
@@ -201,7 +202,13 @@ export class RedisWorkflowService extends PikkuWorkflowService {
|
|
|
201
202
|
)
|
|
202
203
|
}
|
|
203
204
|
|
|
204
|
-
async createRun(
|
|
205
|
+
async createRun(
|
|
206
|
+
workflowName: string,
|
|
207
|
+
input: any,
|
|
208
|
+
inline: boolean,
|
|
209
|
+
graphHash: string,
|
|
210
|
+
wire: WorkflowRunWire
|
|
211
|
+
): Promise<string> {
|
|
205
212
|
const id = randomUUID()
|
|
206
213
|
const now = Date.now()
|
|
207
214
|
|
|
@@ -217,6 +224,12 @@ export class RedisWorkflowService extends PikkuWorkflowService {
|
|
|
217
224
|
'running',
|
|
218
225
|
'input',
|
|
219
226
|
JSON.stringify(input),
|
|
227
|
+
'inline',
|
|
228
|
+
inline ? 'true' : 'false',
|
|
229
|
+
'graphHash',
|
|
230
|
+
graphHash,
|
|
231
|
+
'wire',
|
|
232
|
+
JSON.stringify(wire),
|
|
220
233
|
'createdAt',
|
|
221
234
|
now.toString(),
|
|
222
235
|
'updatedAt',
|
|
@@ -241,6 +254,9 @@ export class RedisWorkflowService extends PikkuWorkflowService {
|
|
|
241
254
|
input: JSON.parse(data.input!),
|
|
242
255
|
output: data.output ? JSON.parse(data.output) : undefined,
|
|
243
256
|
error: data.error ? JSON.parse(data.error) : undefined,
|
|
257
|
+
inline: data.inline === 'true' ? true : undefined,
|
|
258
|
+
graphHash: data.graphHash || undefined,
|
|
259
|
+
wire: data.wire ? JSON.parse(data.wire) : { type: 'unknown' },
|
|
244
260
|
createdAt: new Date(Number(data.createdAt!)),
|
|
245
261
|
updatedAt: new Date(Number(data.updatedAt!)),
|
|
246
262
|
}
|
|
@@ -695,6 +711,195 @@ export class RedisWorkflowService extends PikkuWorkflowService {
|
|
|
695
711
|
}
|
|
696
712
|
}
|
|
697
713
|
|
|
714
|
+
// ============================================================================
|
|
715
|
+
// Workflow Graph Methods
|
|
716
|
+
// ============================================================================
|
|
717
|
+
|
|
718
|
+
async getCompletedGraphState(runId: string): Promise<{
|
|
719
|
+
completedNodeIds: string[]
|
|
720
|
+
failedNodeIds: string[]
|
|
721
|
+
branchKeys: Record<string, string>
|
|
722
|
+
}> {
|
|
723
|
+
const completedNodeIds: string[] = []
|
|
724
|
+
const failedNodeIds: string[] = []
|
|
725
|
+
const branchKeys: Record<string, string> = {}
|
|
726
|
+
|
|
727
|
+
const pattern = `${this.keyPrefix}:step:${runId}:*`
|
|
728
|
+
let cursor = '0'
|
|
729
|
+
|
|
730
|
+
do {
|
|
731
|
+
const [newCursor, foundKeys] = await this.redis.scan(
|
|
732
|
+
cursor,
|
|
733
|
+
'MATCH',
|
|
734
|
+
pattern,
|
|
735
|
+
'COUNT',
|
|
736
|
+
100
|
|
737
|
+
)
|
|
738
|
+
cursor = newCursor
|
|
739
|
+
|
|
740
|
+
for (const key of foundKeys) {
|
|
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(':')
|
|
755
|
+
|
|
756
|
+
if (status === 'succeeded') {
|
|
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)
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
} while (cursor !== '0')
|
|
770
|
+
|
|
771
|
+
return { completedNodeIds, failedNodeIds, branchKeys }
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
async getNodesWithoutSteps(
|
|
775
|
+
runId: string,
|
|
776
|
+
nodeIds: string[]
|
|
777
|
+
): Promise<string[]> {
|
|
778
|
+
if (nodeIds.length === 0) return []
|
|
779
|
+
|
|
780
|
+
const result: string[] = []
|
|
781
|
+
|
|
782
|
+
for (const nodeId of nodeIds) {
|
|
783
|
+
const key = this.stepKey(runId, nodeId)
|
|
784
|
+
const exists = await this.redis.exists(key)
|
|
785
|
+
if (!exists) {
|
|
786
|
+
result.push(nodeId)
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
return result
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
async getNodeResults(
|
|
794
|
+
runId: string,
|
|
795
|
+
nodeIds: string[]
|
|
796
|
+
): Promise<Record<string, any>> {
|
|
797
|
+
if (nodeIds.length === 0) return {}
|
|
798
|
+
|
|
799
|
+
const results: Record<string, any> = {}
|
|
800
|
+
|
|
801
|
+
for (const nodeId of nodeIds) {
|
|
802
|
+
const key = this.stepKey(runId, nodeId)
|
|
803
|
+
const data = await this.redis.hmget(key, 'status', 'result')
|
|
804
|
+
const [status, result] = data
|
|
805
|
+
|
|
806
|
+
if (status === 'succeeded' && result) {
|
|
807
|
+
results[nodeId] = JSON.parse(result)
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
return results
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
async setBranchTaken(stepId: string, branchKey: string): Promise<void> {
|
|
815
|
+
// Extract runId and stepName from stepId (format: runId:stepName:timestamp)
|
|
816
|
+
const parts = stepId.split(':')
|
|
817
|
+
const runId = parts[0]!
|
|
818
|
+
const stepName = parts.slice(1, -1).join(':')
|
|
819
|
+
|
|
820
|
+
const now = Date.now()
|
|
821
|
+
const key = this.stepKey(runId, stepName)
|
|
822
|
+
|
|
823
|
+
await this.redis.hmset(
|
|
824
|
+
key,
|
|
825
|
+
'branchTaken',
|
|
826
|
+
branchKey,
|
|
827
|
+
'updatedAt',
|
|
828
|
+
now.toString()
|
|
829
|
+
)
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
async updateRunState(
|
|
833
|
+
runId: string,
|
|
834
|
+
name: string,
|
|
835
|
+
value: unknown
|
|
836
|
+
): Promise<void> {
|
|
837
|
+
const key = this.runKey(runId)
|
|
838
|
+
const now = Date.now()
|
|
839
|
+
|
|
840
|
+
// Get existing state or create empty object
|
|
841
|
+
const existingState = await this.redis.hget(key, 'state')
|
|
842
|
+
const state = existingState ? JSON.parse(existingState) : {}
|
|
843
|
+
|
|
844
|
+
// Update the specific field
|
|
845
|
+
state[name] = value
|
|
846
|
+
|
|
847
|
+
await this.redis.hmset(
|
|
848
|
+
key,
|
|
849
|
+
'state',
|
|
850
|
+
JSON.stringify(state),
|
|
851
|
+
'updatedAt',
|
|
852
|
+
now.toString()
|
|
853
|
+
)
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
async getRunState(runId: string): Promise<Record<string, unknown>> {
|
|
857
|
+
const key = this.runKey(runId)
|
|
858
|
+
const stateStr = await this.redis.hget(key, 'state')
|
|
859
|
+
if (!stateStr) {
|
|
860
|
+
return {}
|
|
861
|
+
}
|
|
862
|
+
return JSON.parse(stateStr)
|
|
863
|
+
}
|
|
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
|
+
|
|
698
903
|
async close(): Promise<void> {
|
|
699
904
|
if (this.ownsConnection) {
|
|
700
905
|
await this.redis.quit()
|