@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
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { PikkuWorkflowService, } from '@pikku/core/workflow';
|
|
2
|
+
import { Redis } from 'ioredis';
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
4
|
+
export class RedisWorkflowService extends PikkuWorkflowService {
|
|
5
|
+
redis;
|
|
6
|
+
keyPrefix;
|
|
7
|
+
ownsConnection;
|
|
8
|
+
lockTTL = 30000;
|
|
9
|
+
constructor(connectionOrConfig, keyPrefix = 'workflows') {
|
|
10
|
+
super();
|
|
11
|
+
this.keyPrefix = keyPrefix;
|
|
12
|
+
if (connectionOrConfig instanceof Redis) {
|
|
13
|
+
this.redis = connectionOrConfig;
|
|
14
|
+
this.ownsConnection = false;
|
|
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
|
+
runKey(runId) {
|
|
25
|
+
return `${this.keyPrefix}:run:${runId}`;
|
|
26
|
+
}
|
|
27
|
+
stepKey(runId, stepName) {
|
|
28
|
+
return `${this.keyPrefix}:step:${runId}:${stepName}`;
|
|
29
|
+
}
|
|
30
|
+
stepHistoryKey(stepId) {
|
|
31
|
+
return `${this.keyPrefix}:step-history:${stepId}`;
|
|
32
|
+
}
|
|
33
|
+
lockKey(runId) {
|
|
34
|
+
return `${this.keyPrefix}:lock:${runId}`;
|
|
35
|
+
}
|
|
36
|
+
async saveStepHistory(stepId, stepName, attemptCount, status, result, error, retries, retryDelay) {
|
|
37
|
+
const historyKey = this.stepHistoryKey(stepId);
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
const entry = {
|
|
40
|
+
stepId,
|
|
41
|
+
stepName,
|
|
42
|
+
attemptCount,
|
|
43
|
+
status,
|
|
44
|
+
result: result !== undefined ? JSON.stringify(result) : undefined,
|
|
45
|
+
error: error !== undefined ? JSON.stringify(error) : undefined,
|
|
46
|
+
retries,
|
|
47
|
+
retryDelay,
|
|
48
|
+
createdAt: now,
|
|
49
|
+
};
|
|
50
|
+
switch (status) {
|
|
51
|
+
case 'running':
|
|
52
|
+
entry.runningAt = now;
|
|
53
|
+
break;
|
|
54
|
+
case 'scheduled':
|
|
55
|
+
entry.scheduledAt = now;
|
|
56
|
+
break;
|
|
57
|
+
case 'succeeded':
|
|
58
|
+
entry.succeededAt = now;
|
|
59
|
+
break;
|
|
60
|
+
case 'failed':
|
|
61
|
+
entry.failedAt = now;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
Object.keys(entry).forEach((key) => entry[key] === undefined && delete entry[key]);
|
|
65
|
+
await this.redis.zadd(historyKey, attemptCount, JSON.stringify(entry));
|
|
66
|
+
}
|
|
67
|
+
async updateCurrentHistoryRecord(stepId, stepName, attemptCount, status, result, error, retries, retryDelay) {
|
|
68
|
+
const historyKey = this.stepHistoryKey(stepId);
|
|
69
|
+
const historyEntries = await this.redis.zrangebyscore(historyKey, attemptCount, attemptCount);
|
|
70
|
+
if (historyEntries.length === 0) {
|
|
71
|
+
await this.saveStepHistory(stepId, stepName, attemptCount, status, result, error, retries, retryDelay);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const existingEntry = JSON.parse(historyEntries[0]);
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
const updatedEntry = {
|
|
77
|
+
...existingEntry,
|
|
78
|
+
status,
|
|
79
|
+
result: result !== undefined ? JSON.stringify(result) : undefined,
|
|
80
|
+
error: error !== undefined ? JSON.stringify(error) : undefined,
|
|
81
|
+
};
|
|
82
|
+
switch (status) {
|
|
83
|
+
case 'running':
|
|
84
|
+
updatedEntry.runningAt = now;
|
|
85
|
+
break;
|
|
86
|
+
case 'scheduled':
|
|
87
|
+
updatedEntry.scheduledAt = now;
|
|
88
|
+
break;
|
|
89
|
+
case 'succeeded':
|
|
90
|
+
updatedEntry.succeededAt = now;
|
|
91
|
+
break;
|
|
92
|
+
case 'failed':
|
|
93
|
+
updatedEntry.failedAt = now;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
await this.redis.zremrangebyscore(historyKey, attemptCount, attemptCount);
|
|
97
|
+
await this.redis.zadd(historyKey, attemptCount, JSON.stringify(updatedEntry));
|
|
98
|
+
}
|
|
99
|
+
async createRun(workflowName, input) {
|
|
100
|
+
const id = randomUUID();
|
|
101
|
+
const now = Date.now();
|
|
102
|
+
const key = this.runKey(id);
|
|
103
|
+
await this.redis.hmset(key, 'id', id, 'workflow', workflowName, 'status', 'running', 'input', JSON.stringify(input), 'createdAt', now.toString(), 'updatedAt', now.toString());
|
|
104
|
+
return id;
|
|
105
|
+
}
|
|
106
|
+
async getRun(id) {
|
|
107
|
+
const key = this.runKey(id);
|
|
108
|
+
const data = await this.redis.hgetall(key);
|
|
109
|
+
if (!data.id) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
id: data.id,
|
|
114
|
+
workflow: data.workflow,
|
|
115
|
+
status: data.status,
|
|
116
|
+
input: JSON.parse(data.input),
|
|
117
|
+
output: data.output ? JSON.parse(data.output) : undefined,
|
|
118
|
+
error: data.error ? JSON.parse(data.error) : undefined,
|
|
119
|
+
createdAt: new Date(Number(data.createdAt)),
|
|
120
|
+
updatedAt: new Date(Number(data.updatedAt)),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
async updateRunStatus(id, status, output, error) {
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
const key = this.runKey(id);
|
|
126
|
+
const fields = {
|
|
127
|
+
status,
|
|
128
|
+
updatedAt: now.toString(),
|
|
129
|
+
};
|
|
130
|
+
if (output !== undefined) {
|
|
131
|
+
fields.output = JSON.stringify(output);
|
|
132
|
+
}
|
|
133
|
+
if (error !== undefined) {
|
|
134
|
+
fields.error = JSON.stringify(error);
|
|
135
|
+
}
|
|
136
|
+
await this.redis.hmset(key, fields);
|
|
137
|
+
}
|
|
138
|
+
async insertStepState(runId, stepName, rpcName, data, stepOptions) {
|
|
139
|
+
const now = Date.now();
|
|
140
|
+
const stepId = `${runId}:${stepName}:${now}`;
|
|
141
|
+
const key = this.stepKey(runId, stepName);
|
|
142
|
+
const fields = {
|
|
143
|
+
stepId,
|
|
144
|
+
rpcName,
|
|
145
|
+
data: JSON.stringify(data),
|
|
146
|
+
status: 'pending',
|
|
147
|
+
attemptCount: '1',
|
|
148
|
+
createdAt: now.toString(),
|
|
149
|
+
updatedAt: now.toString(),
|
|
150
|
+
};
|
|
151
|
+
if (stepOptions?.retries !== undefined) {
|
|
152
|
+
fields.retries = stepOptions.retries.toString();
|
|
153
|
+
}
|
|
154
|
+
if (stepOptions?.retryDelay !== undefined) {
|
|
155
|
+
fields.retryDelay = stepOptions.retryDelay.toString();
|
|
156
|
+
}
|
|
157
|
+
await this.redis.hmset(key, fields);
|
|
158
|
+
await this.saveStepHistory(stepId, stepName, 1, 'pending', undefined, undefined, stepOptions?.retries, stepOptions?.retryDelay?.toString());
|
|
159
|
+
return {
|
|
160
|
+
stepId,
|
|
161
|
+
status: 'pending',
|
|
162
|
+
attemptCount: 1,
|
|
163
|
+
retries: stepOptions?.retries,
|
|
164
|
+
retryDelay: stepOptions?.retryDelay !== undefined
|
|
165
|
+
? stepOptions.retryDelay.toString()
|
|
166
|
+
: undefined,
|
|
167
|
+
createdAt: new Date(now),
|
|
168
|
+
updatedAt: new Date(now),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
async getStepState(runId, stepName) {
|
|
172
|
+
const key = this.stepKey(runId, stepName);
|
|
173
|
+
const data = await this.redis.hgetall(key);
|
|
174
|
+
if (!data.stepId) {
|
|
175
|
+
throw new Error(`Step not found: runId=${runId}, stepName=${stepName}. Use insertStepState to create it.`);
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
stepId: data.stepId,
|
|
179
|
+
status: data.status,
|
|
180
|
+
result: data.result ? JSON.parse(data.result) : undefined,
|
|
181
|
+
error: data.error ? JSON.parse(data.error) : undefined,
|
|
182
|
+
attemptCount: Number(data.attemptCount || 1),
|
|
183
|
+
retries: data.retries ? Number(data.retries) : undefined,
|
|
184
|
+
retryDelay: data.retryDelay,
|
|
185
|
+
createdAt: new Date(Number(data.createdAt)),
|
|
186
|
+
updatedAt: new Date(Number(data.updatedAt)),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
async getRunHistory(runId) {
|
|
190
|
+
const pattern = `${this.keyPrefix}:step:${runId}:*`;
|
|
191
|
+
const stepKeys = [];
|
|
192
|
+
let cursor = '0';
|
|
193
|
+
do {
|
|
194
|
+
const [newCursor, foundKeys] = await this.redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100);
|
|
195
|
+
cursor = newCursor;
|
|
196
|
+
stepKeys.push(...foundKeys);
|
|
197
|
+
} while (cursor !== '0');
|
|
198
|
+
const allHistoryEntries = [];
|
|
199
|
+
for (const stepKey of stepKeys) {
|
|
200
|
+
const stepData = await this.redis.hgetall(stepKey);
|
|
201
|
+
if (!stepData.stepId)
|
|
202
|
+
continue;
|
|
203
|
+
const stepId = stepData.stepId;
|
|
204
|
+
const historyKey = this.stepHistoryKey(stepId);
|
|
205
|
+
const historyEntries = await this.redis.zrange(historyKey, 0, -1);
|
|
206
|
+
for (const entryStr of historyEntries) {
|
|
207
|
+
const entry = JSON.parse(entryStr);
|
|
208
|
+
allHistoryEntries.push({
|
|
209
|
+
stepId: entry.stepId,
|
|
210
|
+
stepName: entry.stepName,
|
|
211
|
+
status: entry.status,
|
|
212
|
+
result: entry.result ? JSON.parse(entry.result) : undefined,
|
|
213
|
+
error: entry.error ? JSON.parse(entry.error) : undefined,
|
|
214
|
+
attemptCount: entry.attemptCount,
|
|
215
|
+
retries: entry.retries,
|
|
216
|
+
retryDelay: entry.retryDelay,
|
|
217
|
+
createdAt: new Date(entry.createdAt),
|
|
218
|
+
updatedAt: new Date(entry.createdAt),
|
|
219
|
+
runningAt: entry.runningAt ? new Date(entry.runningAt) : undefined,
|
|
220
|
+
scheduledAt: entry.scheduledAt
|
|
221
|
+
? new Date(entry.scheduledAt)
|
|
222
|
+
: undefined,
|
|
223
|
+
succeededAt: entry.succeededAt
|
|
224
|
+
? new Date(entry.succeededAt)
|
|
225
|
+
: undefined,
|
|
226
|
+
failedAt: entry.failedAt ? new Date(entry.failedAt) : undefined,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return allHistoryEntries.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
231
|
+
}
|
|
232
|
+
async setStepRunning(stepId) {
|
|
233
|
+
const parts = stepId.split(':');
|
|
234
|
+
const runId = parts[0];
|
|
235
|
+
const stepName = parts.slice(1, -1).join(':');
|
|
236
|
+
const now = Date.now();
|
|
237
|
+
const key = this.stepKey(runId, stepName);
|
|
238
|
+
const data = await this.redis.hgetall(key);
|
|
239
|
+
const attemptCount = Number(data.attemptCount || 1);
|
|
240
|
+
const retries = data.retries ? Number(data.retries) : undefined;
|
|
241
|
+
const retryDelay = data.retryDelay;
|
|
242
|
+
await this.redis.hmset(key, 'status', 'running', 'updatedAt', now.toString());
|
|
243
|
+
await this.updateCurrentHistoryRecord(stepId, stepName, attemptCount, 'running', undefined, undefined, retries, retryDelay);
|
|
244
|
+
}
|
|
245
|
+
async setStepScheduled(stepId) {
|
|
246
|
+
const parts = stepId.split(':');
|
|
247
|
+
const runId = parts[0];
|
|
248
|
+
const stepName = parts.slice(1, -1).join(':');
|
|
249
|
+
const now = Date.now();
|
|
250
|
+
const key = this.stepKey(runId, stepName);
|
|
251
|
+
const data = await this.redis.hgetall(key);
|
|
252
|
+
const attemptCount = Number(data.attemptCount || 1);
|
|
253
|
+
const retries = data.retries ? Number(data.retries) : undefined;
|
|
254
|
+
const retryDelay = data.retryDelay;
|
|
255
|
+
await this.redis.hmset(key, 'status', 'scheduled', 'updatedAt', now.toString());
|
|
256
|
+
await this.updateCurrentHistoryRecord(stepId, stepName, attemptCount, 'scheduled', undefined, undefined, retries, retryDelay);
|
|
257
|
+
}
|
|
258
|
+
async setStepResult(stepId, result) {
|
|
259
|
+
const parts = stepId.split(':');
|
|
260
|
+
const runId = parts[0];
|
|
261
|
+
const stepName = parts.slice(1, -1).join(':');
|
|
262
|
+
const now = Date.now();
|
|
263
|
+
const key = this.stepKey(runId, stepName);
|
|
264
|
+
const data = await this.redis.hgetall(key);
|
|
265
|
+
const attemptCount = Number(data.attemptCount || 1);
|
|
266
|
+
const retries = data.retries ? Number(data.retries) : undefined;
|
|
267
|
+
const retryDelay = data.retryDelay;
|
|
268
|
+
await this.redis.hmset(key, 'status', 'succeeded', 'result', JSON.stringify(result), 'updatedAt', now.toString());
|
|
269
|
+
await this.redis.hdel(key, 'error');
|
|
270
|
+
await this.updateCurrentHistoryRecord(stepId, stepName, attemptCount, 'succeeded', result, undefined, retries, retryDelay);
|
|
271
|
+
}
|
|
272
|
+
async setStepError(stepId, error) {
|
|
273
|
+
const parts = stepId.split(':');
|
|
274
|
+
const runId = parts[0];
|
|
275
|
+
const stepName = parts.slice(1, -1).join(':');
|
|
276
|
+
const now = Date.now();
|
|
277
|
+
const key = this.stepKey(runId, stepName);
|
|
278
|
+
const data = await this.redis.hgetall(key);
|
|
279
|
+
const attemptCount = Number(data.attemptCount || 1);
|
|
280
|
+
const retries = data.retries ? Number(data.retries) : undefined;
|
|
281
|
+
const retryDelay = data.retryDelay;
|
|
282
|
+
const serializedError = {
|
|
283
|
+
message: error.message,
|
|
284
|
+
stack: error.stack,
|
|
285
|
+
code: error.code,
|
|
286
|
+
};
|
|
287
|
+
await this.redis.hmset(key, 'status', 'failed', 'error', JSON.stringify(serializedError), 'updatedAt', now.toString());
|
|
288
|
+
await this.redis.hdel(key, 'result');
|
|
289
|
+
await this.updateCurrentHistoryRecord(stepId, stepName, attemptCount, 'failed', undefined, serializedError, retries, retryDelay);
|
|
290
|
+
}
|
|
291
|
+
async withRunLock(id, fn) {
|
|
292
|
+
const lockKey = this.lockKey(id);
|
|
293
|
+
const lockValue = randomUUID();
|
|
294
|
+
const maxRetries = 10;
|
|
295
|
+
const retryDelay = 100;
|
|
296
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
297
|
+
const acquired = await this.redis.set(lockKey, lockValue, 'PX', this.lockTTL, 'NX');
|
|
298
|
+
if (acquired === 'OK') {
|
|
299
|
+
try {
|
|
300
|
+
return await fn();
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
await this.redis.eval(`if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end`, 1, lockKey, lockValue);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
307
|
+
}
|
|
308
|
+
throw new Error(`Failed to acquire lock for run ${id} after ${maxRetries} retries`);
|
|
309
|
+
}
|
|
310
|
+
async createRetryAttempt(stepId, status) {
|
|
311
|
+
const parts = stepId.split(':');
|
|
312
|
+
const runId = parts[0];
|
|
313
|
+
const stepName = parts.slice(1, -1).join(':');
|
|
314
|
+
const now = Date.now();
|
|
315
|
+
const key = this.stepKey(runId, stepName);
|
|
316
|
+
const data = await this.redis.hgetall(key);
|
|
317
|
+
const currentAttempt = Number(data.attemptCount || 1);
|
|
318
|
+
const newAttemptCount = currentAttempt + 1;
|
|
319
|
+
const retries = data.retries ? Number(data.retries) : undefined;
|
|
320
|
+
const retryDelay = data.retryDelay;
|
|
321
|
+
await this.redis.hmset(key, 'status', status, 'attemptCount', newAttemptCount.toString(), 'updatedAt', now.toString());
|
|
322
|
+
await this.saveStepHistory(stepId, stepName, newAttemptCount, 'pending', undefined, undefined, retries, retryDelay);
|
|
323
|
+
return {
|
|
324
|
+
stepId: data.stepId,
|
|
325
|
+
status: 'pending',
|
|
326
|
+
result: data.result ? JSON.parse(data.result) : undefined,
|
|
327
|
+
error: data.error ? JSON.parse(data.error) : undefined,
|
|
328
|
+
attemptCount: newAttemptCount,
|
|
329
|
+
retries: retries,
|
|
330
|
+
retryDelay: retryDelay,
|
|
331
|
+
createdAt: new Date(Number(data.createdAt)),
|
|
332
|
+
updatedAt: new Date(now),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
async close() {
|
|
336
|
+
if (this.ownsConnection) {
|
|
337
|
+
await this.redis.quit();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
//# sourceMappingURL=redis-workflow-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-workflow-service.js","sourceRoot":"","sources":["../../src/redis-workflow-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["../src/index.ts","../src/redis-channel-store.ts","../src/redis-eventhub-store.ts","../src/redis-workflow-service.ts"],"version":"5.9.3"}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pikku/redis",
|
|
3
|
+
"version": "0.11.0",
|
|
4
|
+
"author": "yasser.fadl@gmail.com",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"module": "dist/src/index.js",
|
|
7
|
+
"main": "dist/src/index.js",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"tsc": "tsc",
|
|
11
|
+
"ncu": "npx npm-check-updates",
|
|
12
|
+
"build": "tsc -b",
|
|
13
|
+
"test": "bash run-tests.sh",
|
|
14
|
+
"test:watch": "bash run-tests.sh --watch",
|
|
15
|
+
"test:coverage": "bash run-tests.sh --coverage",
|
|
16
|
+
"release": "npm run build && npm test"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"@pikku/core": "^0.11.0"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"ioredis": "^5.4.1"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"typescript": "^5.9"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/run-tests.sh
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Parse command line arguments
|
|
4
|
+
WATCH_MODE=false
|
|
5
|
+
COVERAGE_MODE=false
|
|
6
|
+
|
|
7
|
+
for arg in "$@"
|
|
8
|
+
do
|
|
9
|
+
case $arg in
|
|
10
|
+
--watch)
|
|
11
|
+
WATCH_MODE=true
|
|
12
|
+
shift
|
|
13
|
+
;;
|
|
14
|
+
--coverage)
|
|
15
|
+
COVERAGE_MODE=true
|
|
16
|
+
shift
|
|
17
|
+
;;
|
|
18
|
+
esac
|
|
19
|
+
done
|
|
20
|
+
|
|
21
|
+
# Build the command
|
|
22
|
+
CMD="node --import tsx --test src/**/*.test.ts"
|
|
23
|
+
|
|
24
|
+
if [ "$WATCH_MODE" = true ]; then
|
|
25
|
+
CMD="$CMD --watch"
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
if [ "$COVERAGE_MODE" = true ]; then
|
|
29
|
+
CMD="$CMD --experimental-test-coverage"
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# Execute the command
|
|
33
|
+
eval $CMD
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { CoreUserSession } from '@pikku/core'
|
|
2
|
+
import { Channel, ChannelStore } from '@pikku/core/channel'
|
|
3
|
+
import { Redis, type RedisOptions } from 'ioredis'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Redis-based implementation of ChannelStore
|
|
7
|
+
*
|
|
8
|
+
* Stores channel state in Redis with configurable key prefix.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const redis = new Redis({ host: 'localhost', port: 6379 })
|
|
13
|
+
* const store = new RedisChannelStore(redis, 'myapp')
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export class RedisChannelStore extends ChannelStore {
|
|
17
|
+
private redis: Redis
|
|
18
|
+
private keyPrefix: string
|
|
19
|
+
private ownsConnection: boolean
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param connectionOrConfig - ioredis Redis instance or RedisOptions config
|
|
23
|
+
* @param keyPrefix - Redis key prefix (default: 'pikku')
|
|
24
|
+
*/
|
|
25
|
+
constructor(
|
|
26
|
+
connectionOrConfig: Redis | RedisOptions | string,
|
|
27
|
+
keyPrefix = 'pikku'
|
|
28
|
+
) {
|
|
29
|
+
super()
|
|
30
|
+
this.keyPrefix = keyPrefix
|
|
31
|
+
|
|
32
|
+
// Check if it's a Redis instance or config options
|
|
33
|
+
if (connectionOrConfig instanceof Redis) {
|
|
34
|
+
this.redis = connectionOrConfig
|
|
35
|
+
this.ownsConnection = false
|
|
36
|
+
} else if (typeof connectionOrConfig === 'string') {
|
|
37
|
+
// It's a connection string
|
|
38
|
+
this.redis = new Redis(connectionOrConfig)
|
|
39
|
+
this.ownsConnection = true
|
|
40
|
+
} else {
|
|
41
|
+
// It's a config object
|
|
42
|
+
this.redis = new Redis(connectionOrConfig)
|
|
43
|
+
this.ownsConnection = true
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Initialize the store (no-op for Redis, always ready)
|
|
49
|
+
*/
|
|
50
|
+
public async init(): Promise<void> {
|
|
51
|
+
// Redis doesn't require schema initialization
|
|
52
|
+
// Just verify connection works
|
|
53
|
+
await this.redis.ping()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private channelKey(channelId: string): string {
|
|
57
|
+
return `${this.keyPrefix}:channel:${channelId}`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public async addChannel({
|
|
61
|
+
channelId,
|
|
62
|
+
channelName,
|
|
63
|
+
openingData,
|
|
64
|
+
}: Channel): Promise<void> {
|
|
65
|
+
const key = this.channelKey(channelId)
|
|
66
|
+
|
|
67
|
+
await this.redis.hmset(
|
|
68
|
+
key,
|
|
69
|
+
'channelId',
|
|
70
|
+
channelId,
|
|
71
|
+
'channelName',
|
|
72
|
+
channelName,
|
|
73
|
+
'openingData',
|
|
74
|
+
JSON.stringify(openingData || {}),
|
|
75
|
+
'createdAt',
|
|
76
|
+
Date.now().toString()
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public async removeChannels(channelIds: string[]): Promise<void> {
|
|
81
|
+
if (channelIds.length === 0) {
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const pipeline = this.redis.pipeline()
|
|
86
|
+
|
|
87
|
+
for (const channelId of channelIds) {
|
|
88
|
+
const key = this.channelKey(channelId)
|
|
89
|
+
pipeline.del(key)
|
|
90
|
+
// Also remove from subscriptions
|
|
91
|
+
pipeline.del(`${this.keyPrefix}:subs:${channelId}`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await pipeline.exec()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public async setUserSession(
|
|
98
|
+
channelId: string,
|
|
99
|
+
session: CoreUserSession | null
|
|
100
|
+
): Promise<void> {
|
|
101
|
+
const key = this.channelKey(channelId)
|
|
102
|
+
|
|
103
|
+
if (session) {
|
|
104
|
+
await this.redis.hset(key, 'userSession', JSON.stringify(session))
|
|
105
|
+
} else {
|
|
106
|
+
await this.redis.hdel(key, 'userSession')
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public async getChannelAndSession(
|
|
111
|
+
channelId: string
|
|
112
|
+
): Promise<Channel & { session: CoreUserSession }> {
|
|
113
|
+
const key = this.channelKey(channelId)
|
|
114
|
+
const data = await this.redis.hgetall(key)
|
|
115
|
+
|
|
116
|
+
if (!data.channelId) {
|
|
117
|
+
throw new Error(`Channel not found: ${channelId}`)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
channelId: data.channelId!,
|
|
122
|
+
channelName: data.channelName!,
|
|
123
|
+
openingData: data.openingData ? JSON.parse(data.openingData) : {},
|
|
124
|
+
session: data.userSession ? JSON.parse(data.userSession) : {},
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Close the Redis connection if owned by this store
|
|
130
|
+
*/
|
|
131
|
+
public async close(): Promise<void> {
|
|
132
|
+
if (this.ownsConnection) {
|
|
133
|
+
await this.redis.quit()
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { EventHubStore } from '@pikku/core/channel'
|
|
2
|
+
import { Redis, type RedisOptions } from 'ioredis'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Redis-based implementation of EventHubStore
|
|
6
|
+
*
|
|
7
|
+
* Manages pub/sub topic subscriptions for channels in Redis.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* const redis = new Redis({ host: 'localhost', port: 6379 })
|
|
12
|
+
* const store = new RedisEventHubStore(redis, 'myapp')
|
|
13
|
+
* await store.subscribe('user.123', channelId)
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export class RedisEventHubStore implements EventHubStore {
|
|
17
|
+
private redis: Redis
|
|
18
|
+
private keyPrefix: string
|
|
19
|
+
private ownsConnection: boolean
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param connectionOrConfig - ioredis Redis instance or RedisOptions config
|
|
23
|
+
* @param keyPrefix - Redis key prefix (default: 'pikku')
|
|
24
|
+
*/
|
|
25
|
+
constructor(
|
|
26
|
+
connectionOrConfig: Redis | RedisOptions | string,
|
|
27
|
+
keyPrefix = 'pikku'
|
|
28
|
+
) {
|
|
29
|
+
this.keyPrefix = keyPrefix
|
|
30
|
+
|
|
31
|
+
// Check if it's a Redis instance or config options
|
|
32
|
+
if (connectionOrConfig instanceof Redis) {
|
|
33
|
+
this.redis = connectionOrConfig
|
|
34
|
+
this.ownsConnection = false
|
|
35
|
+
} else if (typeof connectionOrConfig === 'string') {
|
|
36
|
+
// It's a connection string
|
|
37
|
+
this.redis = new Redis(connectionOrConfig)
|
|
38
|
+
this.ownsConnection = true
|
|
39
|
+
} else {
|
|
40
|
+
// It's a config object
|
|
41
|
+
this.redis = new Redis(connectionOrConfig)
|
|
42
|
+
this.ownsConnection = true
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Initialize the store (no-op for Redis, always ready)
|
|
48
|
+
*/
|
|
49
|
+
public async init(): Promise<void> {
|
|
50
|
+
// Redis doesn't require schema initialization
|
|
51
|
+
await this.redis.ping()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private topicKey(topic: string): string {
|
|
55
|
+
return `${this.keyPrefix}:topic:${topic}`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private channelSubsKey(channelId: string): string {
|
|
59
|
+
return `${this.keyPrefix}:subs:${channelId}`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public async getChannelIdsForTopic(topic: string): Promise<string[]> {
|
|
63
|
+
const key = this.topicKey(topic)
|
|
64
|
+
return await this.redis.smembers(key)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public async subscribe(topic: string, channelId: string): Promise<boolean> {
|
|
68
|
+
const topicKey = this.topicKey(topic)
|
|
69
|
+
const subsKey = this.channelSubsKey(channelId)
|
|
70
|
+
|
|
71
|
+
// Add channelId to topic set and topic to channel's subscription set
|
|
72
|
+
const pipeline = this.redis.pipeline()
|
|
73
|
+
pipeline.sadd(topicKey, channelId)
|
|
74
|
+
pipeline.sadd(subsKey, topic)
|
|
75
|
+
|
|
76
|
+
await pipeline.exec()
|
|
77
|
+
|
|
78
|
+
return true
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public async unsubscribe(topic: string, channelId: string): Promise<boolean> {
|
|
82
|
+
const topicKey = this.topicKey(topic)
|
|
83
|
+
const subsKey = this.channelSubsKey(channelId)
|
|
84
|
+
|
|
85
|
+
// Remove channelId from topic set and topic from channel's subscription set
|
|
86
|
+
const pipeline = this.redis.pipeline()
|
|
87
|
+
pipeline.srem(topicKey, channelId)
|
|
88
|
+
pipeline.srem(subsKey, topic)
|
|
89
|
+
|
|
90
|
+
const results = await pipeline.exec()
|
|
91
|
+
|
|
92
|
+
// Check if the channel was actually in the topic set
|
|
93
|
+
return (results![0]![1] as number) > 0
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Close the Redis connection if owned by this store
|
|
98
|
+
*/
|
|
99
|
+
public async close(): Promise<void> {
|
|
100
|
+
if (this.ownsConnection) {
|
|
101
|
+
await this.redis.quit()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|