@uploadista/event-broadcaster-memory 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-memory",
3
3
  "type": "module",
4
- "version": "0.0.13-beta.4",
4
+ "version": "0.0.13",
5
5
  "description": "Memory event broadcaster for Uploadista",
6
6
  "license": "MIT",
7
7
  "author": "Uploadista",
@@ -14,17 +14,22 @@
14
14
  }
15
15
  },
16
16
  "dependencies": {
17
- "effect": "3.19.2",
18
- "@uploadista/core": "0.0.13-beta.4"
17
+ "effect": "3.19.3",
18
+ "@uploadista/core": "0.0.13"
19
19
  },
20
20
  "devDependencies": {
21
- "tsdown": "0.16.0",
22
- "@uploadista/typescript-config": "0.0.13-beta.4"
21
+ "@effect/vitest": "0.27.0",
22
+ "tsdown": "0.16.3",
23
+ "vitest": "4.0.8",
24
+ "@uploadista/typescript-config": "0.0.13"
23
25
  },
24
26
  "scripts": {
25
27
  "build": "tsdown",
26
28
  "format": "biome format --write ./src",
27
29
  "lint": "biome lint --write ./src",
28
- "check": "biome check --write ./src"
30
+ "check": "biome check --write ./src",
31
+ "test": "vitest",
32
+ "test:run": "vitest run",
33
+ "test:watch": "vitest watch"
29
34
  }
30
35
  }
@@ -0,0 +1,348 @@
1
+ import { Effect } from "effect";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { createMemoryEventBroadcaster } from "../src/memory-event-broadcaster";
4
+
5
+ describe("Memory Event Broadcaster", () => {
6
+ describe("Basic Pub/Sub Operations", () => {
7
+ it("should publish and subscribe deliver messages to handlers", async () => {
8
+ const broadcaster = createMemoryEventBroadcaster();
9
+ const receivedMessages: string[] = [];
10
+
11
+ await Effect.runPromise(
12
+ Effect.gen(function* () {
13
+ const handler = (message: string) => {
14
+ receivedMessages.push(message);
15
+ };
16
+
17
+ yield* broadcaster.subscribe("test-channel", handler);
18
+ yield* broadcaster.publish("test-channel", "test-message");
19
+
20
+ expect(receivedMessages).toHaveLength(1);
21
+ expect(receivedMessages[0]).toBe("test-message");
22
+ }),
23
+ );
24
+ });
25
+
26
+ it("should handle multiple subscribers receiving the same message", async () => {
27
+ const broadcaster = createMemoryEventBroadcaster();
28
+ const handler1Messages: string[] = [];
29
+ const handler2Messages: string[] = [];
30
+
31
+ await Effect.runPromise(
32
+ Effect.gen(function* () {
33
+ const handler1 = (message: string) => handler1Messages.push(message);
34
+ const handler2 = (message: string) => handler2Messages.push(message);
35
+
36
+ yield* broadcaster.subscribe("events", handler1);
37
+ yield* broadcaster.subscribe("events", handler2);
38
+ yield* broadcaster.publish("events", "shared-message");
39
+
40
+ expect(handler1Messages).toHaveLength(1);
41
+ expect(handler1Messages[0]).toBe("shared-message");
42
+ expect(handler2Messages).toHaveLength(1);
43
+ expect(handler2Messages[0]).toBe("shared-message");
44
+ }),
45
+ );
46
+ });
47
+
48
+ it("should unsubscribe and remove handler from channel", async () => {
49
+ const broadcaster = createMemoryEventBroadcaster();
50
+ const receivedMessages: string[] = [];
51
+
52
+ await Effect.runPromise(
53
+ Effect.gen(function* () {
54
+ const handler = (message: string) => {
55
+ receivedMessages.push(message);
56
+ };
57
+
58
+ yield* broadcaster.subscribe("channel-1", handler);
59
+ yield* broadcaster.publish("channel-1", "message-1");
60
+
61
+ yield* broadcaster.unsubscribe!("channel-1");
62
+ yield* broadcaster.publish("channel-1", "message-2");
63
+
64
+ expect(receivedMessages).toHaveLength(1);
65
+ expect(receivedMessages[0]).toBe("message-1");
66
+ }),
67
+ );
68
+ });
69
+
70
+ it("should maintain channel isolation", async () => {
71
+ const broadcaster = createMemoryEventBroadcaster();
72
+ const channelAMessages: string[] = [];
73
+ const channelBMessages: string[] = [];
74
+
75
+ await Effect.runPromise(
76
+ Effect.gen(function* () {
77
+ const handlerA = (message: string) => channelAMessages.push(message);
78
+ const handlerB = (message: string) => channelBMessages.push(message);
79
+
80
+ yield* broadcaster.subscribe("channel-a", handlerA);
81
+ yield* broadcaster.subscribe("channel-b", handlerB);
82
+ yield* broadcaster.publish("channel-a", "message-for-a");
83
+
84
+ expect(channelAMessages).toHaveLength(1);
85
+ expect(channelAMessages[0]).toBe("message-for-a");
86
+ expect(channelBMessages).toHaveLength(0);
87
+ }),
88
+ );
89
+ });
90
+
91
+ it("should succeed when subscribing to non-existent channel", async () => {
92
+ const broadcaster = createMemoryEventBroadcaster();
93
+
94
+ await Effect.runPromise(
95
+ Effect.gen(function* () {
96
+ const handler = vi.fn();
97
+ yield* broadcaster.subscribe("non-existent", handler);
98
+
99
+ // Should not throw and handler should be registered
100
+ expect(handler).not.toHaveBeenCalled();
101
+ }),
102
+ );
103
+ });
104
+
105
+ it("should succeed when publishing to channel with no subscribers", async () => {
106
+ const broadcaster = createMemoryEventBroadcaster();
107
+
108
+ await Effect.runPromise(
109
+ Effect.gen(function* () {
110
+ // Should not throw
111
+ yield* broadcaster.publish("empty-channel", "message");
112
+ }),
113
+ );
114
+ });
115
+ });
116
+
117
+ describe("Concurrent Operations", () => {
118
+ it("should handle concurrent publishes to same channel", async () => {
119
+ const broadcaster = createMemoryEventBroadcaster();
120
+ const receivedMessages: string[] = [];
121
+
122
+ await Effect.runPromise(
123
+ Effect.gen(function* () {
124
+ const handler = (message: string) => {
125
+ receivedMessages.push(message);
126
+ };
127
+
128
+ yield* broadcaster.subscribe("concurrent-channel", handler);
129
+
130
+ const messages = Array.from({ length: 10 }, (_, i) => `message-${i}`);
131
+ yield* Effect.all(
132
+ messages.map((msg) => broadcaster.publish("concurrent-channel", msg)),
133
+ { concurrency: "unbounded" },
134
+ );
135
+
136
+ expect(receivedMessages).toHaveLength(10);
137
+ // All messages should be received (order may vary in concurrent execution)
138
+ for (const msg of messages) {
139
+ expect(receivedMessages).toContain(msg);
140
+ }
141
+ }),
142
+ );
143
+ });
144
+
145
+ it("should handle concurrent subscribes to different channels", async () => {
146
+ const broadcaster = createMemoryEventBroadcaster();
147
+ const handlers = Array.from({ length: 5 }, () => vi.fn());
148
+
149
+ await Effect.runPromise(
150
+ Effect.gen(function* () {
151
+ yield* Effect.all(
152
+ handlers.map((handler, i) =>
153
+ broadcaster.subscribe(`channel-${i}`, handler),
154
+ ),
155
+ { concurrency: "unbounded" },
156
+ );
157
+
158
+ // Verify each channel can receive messages independently
159
+ for (let i = 0; i < handlers.length; i++) {
160
+ yield* broadcaster.publish(`channel-${i}`, `message-${i}`);
161
+ }
162
+
163
+ for (let i = 0; i < handlers.length; i++) {
164
+ expect(handlers[i]).toHaveBeenCalledWith(`message-${i}`);
165
+ expect(handlers[i]).toHaveBeenCalledTimes(1);
166
+ }
167
+ }),
168
+ );
169
+ });
170
+
171
+ it("should handle subscribe and publish happening concurrently", async () => {
172
+ const broadcaster = createMemoryEventBroadcaster();
173
+ const receivedMessages: string[] = [];
174
+
175
+ await Effect.runPromise(
176
+ Effect.gen(function* () {
177
+ const handler = (message: string) => {
178
+ receivedMessages.push(message);
179
+ };
180
+
181
+ // Subscribe and publish concurrently
182
+ yield* Effect.all(
183
+ [
184
+ broadcaster.subscribe("concurrent", handler),
185
+ Effect.sleep(5).pipe(
186
+ Effect.flatMap(() => broadcaster.publish("concurrent", "msg1")),
187
+ ),
188
+ Effect.sleep(10).pipe(
189
+ Effect.flatMap(() => broadcaster.publish("concurrent", "msg2")),
190
+ ),
191
+ ],
192
+ { concurrency: "unbounded" },
193
+ );
194
+
195
+ // Wait a bit for messages to be delivered
196
+ yield* Effect.sleep(20);
197
+
198
+ // At least one message should be received
199
+ expect(receivedMessages.length).toBeGreaterThan(0);
200
+ }),
201
+ );
202
+ });
203
+ });
204
+
205
+ describe("Edge Cases", () => {
206
+ it("should deliver empty message string correctly", async () => {
207
+ const broadcaster = createMemoryEventBroadcaster();
208
+ const receivedMessages: string[] = [];
209
+
210
+ await Effect.runPromise(
211
+ Effect.gen(function* () {
212
+ const handler = (message: string) => {
213
+ receivedMessages.push(message);
214
+ };
215
+
216
+ yield* broadcaster.subscribe("empty-test", handler);
217
+ yield* broadcaster.publish("empty-test", "");
218
+
219
+ expect(receivedMessages).toHaveLength(1);
220
+ expect(receivedMessages[0]).toBe("");
221
+ }),
222
+ );
223
+ });
224
+
225
+ it("should handle large message payload", async () => {
226
+ const broadcaster = createMemoryEventBroadcaster();
227
+ const largeMessage = "x".repeat(100000);
228
+ const receivedMessages: string[] = [];
229
+
230
+ await Effect.runPromise(
231
+ Effect.gen(function* () {
232
+ const handler = (message: string) => {
233
+ receivedMessages.push(message);
234
+ };
235
+
236
+ yield* broadcaster.subscribe("large", handler);
237
+ yield* broadcaster.publish("large", largeMessage);
238
+
239
+ expect(receivedMessages).toHaveLength(1);
240
+ expect(receivedMessages[0]).toBe(largeMessage);
241
+ expect(receivedMessages[0].length).toBe(100000);
242
+ }),
243
+ );
244
+ });
245
+
246
+ it("should handle special characters in channel names", async () => {
247
+ const broadcaster = createMemoryEventBroadcaster();
248
+ const receivedMessages: string[] = [];
249
+
250
+ await Effect.runPromise(
251
+ Effect.gen(function* () {
252
+ const handler = (message: string) => {
253
+ receivedMessages.push(message);
254
+ };
255
+
256
+ const specialChannel = "channel:with:colons:and-dashes_and_underscores";
257
+ yield* broadcaster.subscribe(specialChannel, handler);
258
+ yield* broadcaster.publish(specialChannel, "test");
259
+
260
+ expect(receivedMessages).toHaveLength(1);
261
+ expect(receivedMessages[0]).toBe("test");
262
+ }),
263
+ );
264
+ });
265
+
266
+ it("should handle special characters in messages", async () => {
267
+ const broadcaster = createMemoryEventBroadcaster();
268
+ const receivedMessages: string[] = [];
269
+
270
+ await Effect.runPromise(
271
+ Effect.gen(function* () {
272
+ const handler = (message: string) => {
273
+ receivedMessages.push(message);
274
+ };
275
+
276
+ yield* broadcaster.subscribe("special", handler);
277
+
278
+ const specialMessage = 'Hello "World" with \'quotes\' and \n newlines \t tabs 世界 🌍';
279
+ yield* broadcaster.publish("special", specialMessage);
280
+
281
+ expect(receivedMessages).toHaveLength(1);
282
+ expect(receivedMessages[0]).toBe(specialMessage);
283
+ }),
284
+ );
285
+ });
286
+ });
287
+
288
+ describe("Handler Behavior", () => {
289
+ it.skip("should propagate handler exceptions when they occur", async () => {
290
+ // Skipped: Handler exceptions are propagated synchronously in memory broadcaster
291
+ // This is implementation-specific behavior
292
+ const broadcaster = createMemoryEventBroadcaster();
293
+
294
+ await Effect.runPromise(
295
+ Effect.gen(function* () {
296
+ const badHandler = () => {
297
+ throw new Error("Handler error");
298
+ };
299
+
300
+ yield* broadcaster.subscribe("error-test", badHandler);
301
+
302
+ // Handler errors will propagate
303
+ const result = yield* Effect.either(
304
+ broadcaster.publish("error-test", "test-message"),
305
+ );
306
+
307
+ expect(result._tag).toBe("Left");
308
+ }),
309
+ );
310
+ });
311
+
312
+ it("should call handler with exact message string", async () => {
313
+ const broadcaster = createMemoryEventBroadcaster();
314
+ const handler = vi.fn();
315
+
316
+ await Effect.runPromise(
317
+ Effect.gen(function* () {
318
+ yield* broadcaster.subscribe("exact", handler);
319
+ yield* broadcaster.publish("exact", "exact-message");
320
+
321
+ expect(handler).toHaveBeenCalledWith("exact-message");
322
+ expect(handler).toHaveBeenCalledTimes(1);
323
+ }),
324
+ );
325
+ });
326
+
327
+ it("should deliver multiple messages in order", async () => {
328
+ const broadcaster = createMemoryEventBroadcaster();
329
+ const receivedMessages: string[] = [];
330
+
331
+ await Effect.runPromise(
332
+ Effect.gen(function* () {
333
+ const handler = (message: string) => {
334
+ receivedMessages.push(message);
335
+ };
336
+
337
+ yield* broadcaster.subscribe("ordered", handler);
338
+
339
+ yield* broadcaster.publish("ordered", "first");
340
+ yield* broadcaster.publish("ordered", "second");
341
+ yield* broadcaster.publish("ordered", "third");
342
+
343
+ expect(receivedMessages).toEqual(["first", "second", "third"]);
344
+ }),
345
+ );
346
+ });
347
+ });
348
+ });
@@ -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
+ });