@uploadista/event-broadcaster-redis 0.0.13-beta.4 → 0.0.13

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@uploadista/event-broadcaster-redis",
3
3
  "type": "module",
4
- "version": "0.0.13-beta.4",
4
+ "version": "0.0.13",
5
5
  "description": "Redis event broadcaster for Uploadista",
6
6
  "license": "MIT",
7
7
  "author": "Uploadista",
@@ -14,19 +14,27 @@
14
14
  }
15
15
  },
16
16
  "dependencies": {
17
- "@redis/client": "5.9.0",
18
- "effect": "3.19.2",
19
- "@uploadista/core": "0.0.13-beta.4"
17
+ "effect": "3.19.3",
18
+ "@uploadista/core": "0.0.13"
19
+ },
20
+ "peerDependencies": {
21
+ "@redis/client": ">=5.8.0 <6.0.0"
20
22
  },
21
23
  "devDependencies": {
22
- "@types/node": "24.10.0",
23
- "tsdown": "0.16.0",
24
- "@uploadista/typescript-config": "0.0.13-beta.4"
24
+ "@effect/vitest": "0.27.0",
25
+ "@redis/client": "5.9.0",
26
+ "@types/node": "24.10.1",
27
+ "tsdown": "0.16.3",
28
+ "vitest": "4.0.8",
29
+ "@uploadista/typescript-config": "0.0.13"
25
30
  },
26
31
  "scripts": {
27
32
  "build": "tsdown",
28
33
  "format": "biome format --write ./src",
29
34
  "lint": "biome lint --write ./src",
30
- "check": "biome check --write ./src"
35
+ "check": "biome check --write ./src",
36
+ "test": "vitest",
37
+ "test:run": "vitest run",
38
+ "test:watch": "vitest watch"
31
39
  }
32
40
  }
@@ -0,0 +1,322 @@
1
+ import { Effect } from "effect";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { createRedisEventBroadcaster } from "../src/redis-event-broadcaster";
4
+ import {
5
+ createMockRedisClient,
6
+ mockRedisClientAsType,
7
+ } from "./utils/mock-redis";
8
+
9
+ describe("Redis Event Broadcaster", () => {
10
+ describe("Basic Operations", () => {
11
+ it("should call redis.publish with correct channel and message", async () => {
12
+ const mockRedis = createMockRedisClient();
13
+ const mockSubscriber = createMockRedisClient();
14
+
15
+ const broadcaster = createRedisEventBroadcaster({
16
+ redis: mockRedisClientAsType(mockRedis),
17
+ subscriberRedis: mockRedisClientAsType(mockSubscriber),
18
+ });
19
+
20
+ await Effect.runPromise(
21
+ Effect.gen(function* () {
22
+ yield* broadcaster.publish("test-channel", "test-message");
23
+
24
+ expect(mockRedis.publish).toHaveBeenCalledWith(
25
+ "test-channel",
26
+ "test-message",
27
+ );
28
+ expect(mockRedis.publish).toHaveBeenCalledTimes(1);
29
+ }),
30
+ );
31
+ });
32
+
33
+ it("should return void on successful publish", async () => {
34
+ const mockRedis = createMockRedisClient();
35
+ const mockSubscriber = createMockRedisClient();
36
+
37
+ const broadcaster = createRedisEventBroadcaster({
38
+ redis: mockRedisClientAsType(mockRedis),
39
+ subscriberRedis: mockRedisClientAsType(mockSubscriber),
40
+ });
41
+
42
+ await Effect.runPromise(
43
+ Effect.gen(function* () {
44
+ const result = yield* broadcaster.publish("channel", "message");
45
+ expect(result).toBeUndefined();
46
+ }),
47
+ );
48
+ });
49
+
50
+ it("should call subscriberRedis.subscribe with correct parameters", async () => {
51
+ const mockRedis = createMockRedisClient();
52
+ const mockSubscriber = createMockRedisClient();
53
+
54
+ const broadcaster = createRedisEventBroadcaster({
55
+ redis: mockRedisClientAsType(mockRedis),
56
+ subscriberRedis: mockRedisClientAsType(mockSubscriber),
57
+ });
58
+
59
+ await Effect.runPromise(
60
+ Effect.gen(function* () {
61
+ const handler = vi.fn();
62
+ yield* broadcaster.subscribe("my-channel", handler);
63
+
64
+ expect(mockSubscriber.subscribe).toHaveBeenCalledTimes(1);
65
+ expect(mockSubscriber.subscribe).toHaveBeenCalledWith(
66
+ "my-channel",
67
+ expect.any(Function),
68
+ );
69
+ }),
70
+ );
71
+ });
72
+
73
+ it("should invoke handler when message arrives", async () => {
74
+ const mockRedis = createMockRedisClient();
75
+ const mockSubscriber = createMockRedisClient();
76
+
77
+ const broadcaster = createRedisEventBroadcaster({
78
+ redis: mockRedisClientAsType(mockRedis),
79
+ subscriberRedis: mockRedisClientAsType(mockSubscriber),
80
+ });
81
+
82
+ await Effect.runPromise(
83
+ Effect.gen(function* () {
84
+ const handler = vi.fn();
85
+ yield* broadcaster.subscribe("events", handler);
86
+
87
+ // Simulate Redis message
88
+ mockSubscriber.triggerMessage("events", "incoming-message");
89
+
90
+ expect(handler).toHaveBeenCalledWith("incoming-message");
91
+ expect(handler).toHaveBeenCalledTimes(1);
92
+ }),
93
+ );
94
+ });
95
+
96
+ it("should call subscriberRedis.unsubscribe", async () => {
97
+ const mockRedis = createMockRedisClient();
98
+ const mockSubscriber = createMockRedisClient();
99
+
100
+ const broadcaster = createRedisEventBroadcaster({
101
+ redis: mockRedisClientAsType(mockRedis),
102
+ subscriberRedis: mockRedisClientAsType(mockSubscriber),
103
+ });
104
+
105
+ await Effect.runPromise(
106
+ Effect.gen(function* () {
107
+ yield* broadcaster.unsubscribe!("channel-1");
108
+
109
+ expect(mockSubscriber.unsubscribe).toHaveBeenCalledWith("channel-1");
110
+ expect(mockSubscriber.unsubscribe).toHaveBeenCalledTimes(1);
111
+ }),
112
+ );
113
+ });
114
+ });
115
+
116
+ describe("Error Handling", () => {
117
+ it("should return UploadistaError when publish fails", async () => {
118
+ const mockRedis = createMockRedisClient();
119
+ const mockSubscriber = createMockRedisClient();
120
+
121
+ mockRedis.publish.mockRejectedValueOnce(new Error("Connection failed"));
122
+
123
+ const broadcaster = createRedisEventBroadcaster({
124
+ redis: mockRedisClientAsType(mockRedis),
125
+ subscriberRedis: mockRedisClientAsType(mockSubscriber),
126
+ });
127
+
128
+ await Effect.runPromise(
129
+ Effect.gen(function* () {
130
+ const result = yield* Effect.either(
131
+ broadcaster.publish("channel", "message"),
132
+ );
133
+
134
+ expect(result._tag).toBe("Left");
135
+ if (result._tag === "Left") {
136
+ expect(result.left.code).toBe("UNKNOWN_ERROR");
137
+ }
138
+ }),
139
+ );
140
+ });
141
+
142
+ it("should return UploadistaError when subscribe fails", async () => {
143
+ const mockRedis = createMockRedisClient();
144
+ const mockSubscriber = createMockRedisClient();
145
+
146
+ mockSubscriber.subscribe.mockRejectedValueOnce(
147
+ new Error("Subscribe failed"),
148
+ );
149
+
150
+ const broadcaster = createRedisEventBroadcaster({
151
+ redis: mockRedisClientAsType(mockRedis),
152
+ subscriberRedis: mockRedisClientAsType(mockSubscriber),
153
+ });
154
+
155
+ await Effect.runPromise(
156
+ Effect.gen(function* () {
157
+ const result = yield* Effect.either(
158
+ broadcaster.subscribe("channel", vi.fn()),
159
+ );
160
+
161
+ expect(result._tag).toBe("Left");
162
+ if (result._tag === "Left") {
163
+ expect(result.left.code).toBe("UNKNOWN_ERROR");
164
+ }
165
+ }),
166
+ );
167
+ });
168
+
169
+ it("should return UploadistaError when unsubscribe fails", async () => {
170
+ const mockRedis = createMockRedisClient();
171
+ const mockSubscriber = createMockRedisClient();
172
+
173
+ mockSubscriber.unsubscribe.mockRejectedValueOnce(
174
+ new Error("Unsubscribe failed"),
175
+ );
176
+
177
+ const broadcaster = createRedisEventBroadcaster({
178
+ redis: mockRedisClientAsType(mockRedis),
179
+ subscriberRedis: mockRedisClientAsType(mockSubscriber),
180
+ });
181
+
182
+ await Effect.runPromise(
183
+ Effect.gen(function* () {
184
+ const result = yield* Effect.either(broadcaster.unsubscribe!("channel"));
185
+
186
+ expect(result._tag).toBe("Left");
187
+ if (result._tag === "Left") {
188
+ expect(result.left.code).toBe("UNKNOWN_ERROR");
189
+ }
190
+ }),
191
+ );
192
+ });
193
+
194
+ it("should register error handler for redis client", async () => {
195
+ const mockRedis = createMockRedisClient();
196
+ const mockSubscriber = createMockRedisClient();
197
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
198
+
199
+ createRedisEventBroadcaster({
200
+ redis: mockRedisClientAsType(mockRedis),
201
+ subscriberRedis: mockRedisClientAsType(mockSubscriber),
202
+ });
203
+
204
+ expect(mockRedis.on).toHaveBeenCalledWith("error", expect.any(Function));
205
+
206
+ // Trigger error
207
+ mockRedis.triggerError(new Error("Redis error"));
208
+ expect(consoleSpy).toHaveBeenCalledWith(
209
+ "[Redis] Error:",
210
+ expect.any(Error),
211
+ );
212
+
213
+ consoleSpy.mockRestore();
214
+ });
215
+
216
+ it("should register error handler for subscriber client", async () => {
217
+ const mockRedis = createMockRedisClient();
218
+ const mockSubscriber = createMockRedisClient();
219
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
220
+
221
+ createRedisEventBroadcaster({
222
+ redis: mockRedisClientAsType(mockRedis),
223
+ subscriberRedis: mockRedisClientAsType(mockSubscriber),
224
+ });
225
+
226
+ expect(mockSubscriber.on).toHaveBeenCalledWith(
227
+ "error",
228
+ expect.any(Function),
229
+ );
230
+
231
+ // Trigger error
232
+ mockSubscriber.triggerError(new Error("Subscriber error"));
233
+ expect(consoleSpy).toHaveBeenCalledWith(
234
+ "[Redis] Subscriber Error:",
235
+ expect.any(Error),
236
+ );
237
+
238
+ consoleSpy.mockRestore();
239
+ });
240
+ });
241
+
242
+ describe("Redis-Specific Behavior", () => {
243
+ it("should use separate redis and subscriberRedis clients", async () => {
244
+ const mockRedis = createMockRedisClient();
245
+ const mockSubscriber = createMockRedisClient();
246
+
247
+ const broadcaster = createRedisEventBroadcaster({
248
+ redis: mockRedisClientAsType(mockRedis),
249
+ subscriberRedis: mockRedisClientAsType(mockSubscriber),
250
+ });
251
+
252
+ await Effect.runPromise(
253
+ Effect.gen(function* () {
254
+ yield* broadcaster.publish("channel", "message");
255
+ yield* broadcaster.subscribe("channel", vi.fn());
256
+
257
+ // Publish uses redis client
258
+ expect(mockRedis.publish).toHaveBeenCalledTimes(1);
259
+ expect(mockSubscriber.publish).not.toHaveBeenCalled();
260
+
261
+ // Subscribe uses subscriberRedis client
262
+ expect(mockSubscriber.subscribe).toHaveBeenCalledTimes(1);
263
+ expect(mockRedis.subscribe).not.toHaveBeenCalled();
264
+ }),
265
+ );
266
+ });
267
+
268
+ it("should support multiple subscribe calls with different handlers", async () => {
269
+ const mockRedis = createMockRedisClient();
270
+ const mockSubscriber = createMockRedisClient();
271
+
272
+ const broadcaster = createRedisEventBroadcaster({
273
+ redis: mockRedisClientAsType(mockRedis),
274
+ subscriberRedis: mockRedisClientAsType(mockSubscriber),
275
+ });
276
+
277
+ await Effect.runPromise(
278
+ Effect.gen(function* () {
279
+ const handler1 = vi.fn();
280
+ const handler2 = vi.fn();
281
+
282
+ yield* broadcaster.subscribe("channel-1", handler1);
283
+ yield* broadcaster.subscribe("channel-2", handler2);
284
+
285
+ expect(mockSubscriber.subscribe).toHaveBeenCalledTimes(2);
286
+ expect(mockSubscriber.subscribe).toHaveBeenNthCalledWith(
287
+ 1,
288
+ "channel-1",
289
+ expect.any(Function),
290
+ );
291
+ expect(mockSubscriber.subscribe).toHaveBeenNthCalledWith(
292
+ 2,
293
+ "channel-2",
294
+ expect.any(Function),
295
+ );
296
+ }),
297
+ );
298
+ });
299
+
300
+ it("should pass correct channel and message to callback", async () => {
301
+ const mockRedis = createMockRedisClient();
302
+ const mockSubscriber = createMockRedisClient();
303
+
304
+ const broadcaster = createRedisEventBroadcaster({
305
+ redis: mockRedisClientAsType(mockRedis),
306
+ subscriberRedis: mockRedisClientAsType(mockSubscriber),
307
+ });
308
+
309
+ await Effect.runPromise(
310
+ Effect.gen(function* () {
311
+ const handler = vi.fn();
312
+ yield* broadcaster.subscribe("test-channel", handler);
313
+
314
+ // Simulate Redis delivering message with channel info
315
+ mockSubscriber.triggerMessage("test-channel", "test-payload");
316
+
317
+ expect(handler).toHaveBeenCalledWith("test-payload");
318
+ }),
319
+ );
320
+ });
321
+ });
322
+ });
@@ -0,0 +1,58 @@
1
+ import type { RedisClientType } from "@redis/client";
2
+ import { vi } from "vitest";
3
+
4
+ type MessageCallback = (message: string, channel: string) => void;
5
+
6
+ export interface MockRedisClient {
7
+ publish: ReturnType<typeof vi.fn>;
8
+ subscribe: ReturnType<typeof vi.fn>;
9
+ unsubscribe: ReturnType<typeof vi.fn>;
10
+ on: ReturnType<typeof vi.fn>;
11
+ triggerMessage: (channel: string, message: string) => void;
12
+ triggerError: (error: Error) => void;
13
+ }
14
+
15
+ export function createMockRedisClient(): MockRedisClient {
16
+ const subscribers = new Map<string, Set<MessageCallback>>();
17
+ let errorHandler: ((error: Error) => void) | null = null;
18
+
19
+ return {
20
+ publish: vi.fn().mockResolvedValue(1),
21
+
22
+ subscribe: vi.fn().mockImplementation(
23
+ async (channel: string, callback: MessageCallback) => {
24
+ if (!subscribers.has(channel)) {
25
+ subscribers.set(channel, new Set());
26
+ }
27
+ subscribers.get(channel)?.add(callback);
28
+ },
29
+ ),
30
+
31
+ unsubscribe: vi.fn().mockResolvedValue(undefined),
32
+
33
+ on: vi.fn().mockImplementation((event: string, handler: (error: Error) => void) => {
34
+ if (event === "error") {
35
+ errorHandler = handler;
36
+ }
37
+ }),
38
+
39
+ triggerMessage: (channel: string, message: string) => {
40
+ const channelSubs = subscribers.get(channel);
41
+ if (channelSubs) {
42
+ for (const callback of channelSubs) {
43
+ callback(message, channel);
44
+ }
45
+ }
46
+ },
47
+
48
+ triggerError: (error: Error) => {
49
+ if (errorHandler) {
50
+ errorHandler(error);
51
+ }
52
+ },
53
+ };
54
+ }
55
+
56
+ export function mockRedisClientAsType(mock: MockRedisClient): RedisClientType {
57
+ return mock as unknown as RedisClientType;
58
+ }
@@ -0,0 +1,39 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ /**
4
+ * Shared vitest configuration template for uploadista-sdk packages
5
+ *
6
+ * This template should be used by all SDK packages to ensure consistent
7
+ * testing configuration across the monorepo.
8
+ *
9
+ * Key features:
10
+ * - Tests in dedicated `tests/` directories (not colocated with src)
11
+ * - Node environment for server-side code
12
+ * - V8 coverage provider
13
+ * - Global test functions available
14
+ * - Effect testing support via @effect/vitest
15
+ *
16
+ * Usage:
17
+ * Copy this file to your package root as `vitest.config.ts` and customize
18
+ * if needed (though most packages should use this as-is).
19
+ */
20
+ export default defineConfig({
21
+ test: {
22
+ globals: true,
23
+ environment: "node",
24
+ include: ["tests/**/*.test.ts"],
25
+ exclude: ["node_modules", "dist"],
26
+ coverage: {
27
+ provider: "v8",
28
+ reporter: ["text", "json", "html"],
29
+ exclude: [
30
+ "node_modules/",
31
+ "dist/",
32
+ "**/*.d.ts",
33
+ "**/*.test.ts",
34
+ "**/*.spec.ts",
35
+ "tests/",
36
+ ],
37
+ },
38
+ },
39
+ });