@onebun/core 0.1.0 → 0.1.2

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.
@@ -0,0 +1,246 @@
1
+ /**
2
+ * In-Memory WebSocket Storage Tests
3
+ */
4
+
5
+ import {
6
+ describe,
7
+ it,
8
+ expect,
9
+ beforeEach,
10
+ } from 'bun:test';
11
+
12
+ import type { WsClientData, WsRoom } from './ws.types';
13
+
14
+ import { InMemoryWsStorage } from './ws-storage-memory';
15
+
16
+ describe('InMemoryWsStorage', () => {
17
+ let storage: InMemoryWsStorage;
18
+
19
+ beforeEach(async () => {
20
+ storage = new InMemoryWsStorage();
21
+ await storage.clear();
22
+ });
23
+
24
+ describe('client operations', () => {
25
+ const createClient = (id: string): WsClientData => ({
26
+ id,
27
+ rooms: [],
28
+ connectedAt: Date.now(),
29
+ auth: null,
30
+ metadata: {},
31
+ });
32
+
33
+ it('should add and retrieve a client', async () => {
34
+ const client = createClient('client1');
35
+ await storage.addClient(client);
36
+
37
+ const retrieved = await storage.getClient('client1');
38
+ expect(retrieved).not.toBeNull();
39
+ expect(retrieved?.id).toBe('client1');
40
+ });
41
+
42
+ it('should return null for non-existent client', async () => {
43
+ const result = await storage.getClient('non-existent');
44
+ expect(result).toBeNull();
45
+ });
46
+
47
+ it('should update client data', async () => {
48
+ const client = createClient('client1');
49
+ await storage.addClient(client);
50
+
51
+ await storage.updateClient('client1', { metadata: { custom: 'value' } });
52
+
53
+ const updated = await storage.getClient('client1');
54
+ expect(updated?.metadata).toEqual({ custom: 'value' });
55
+ });
56
+
57
+ it('should remove a client', async () => {
58
+ const client = createClient('client1');
59
+ await storage.addClient(client);
60
+
61
+ await storage.removeClient('client1');
62
+
63
+ const result = await storage.getClient('client1');
64
+ expect(result).toBeNull();
65
+ });
66
+
67
+ it('should get all clients', async () => {
68
+ await storage.addClient(createClient('client1'));
69
+ await storage.addClient(createClient('client2'));
70
+
71
+ const clients = await storage.getAllClients();
72
+ expect(clients).toHaveLength(2);
73
+ });
74
+
75
+ it('should get client count', async () => {
76
+ await storage.addClient(createClient('client1'));
77
+ await storage.addClient(createClient('client2'));
78
+
79
+ const count = await storage.getClientCount();
80
+ expect(count).toBe(2);
81
+ });
82
+ });
83
+
84
+ describe('room operations', () => {
85
+ it('should create and retrieve a room', async () => {
86
+ const room: WsRoom = { name: 'test-room', clientIds: [] };
87
+ await storage.createRoom(room);
88
+
89
+ const retrieved = await storage.getRoom('test-room');
90
+ expect(retrieved).not.toBeNull();
91
+ expect(retrieved?.name).toBe('test-room');
92
+ });
93
+
94
+ it('should delete a room', async () => {
95
+ const room: WsRoom = { name: 'test-room', clientIds: [] };
96
+ await storage.createRoom(room);
97
+
98
+ await storage.deleteRoom('test-room');
99
+
100
+ const result = await storage.getRoom('test-room');
101
+ expect(result).toBeNull();
102
+ });
103
+
104
+ it('should get all rooms', async () => {
105
+ await storage.createRoom({ name: 'room1', clientIds: [] });
106
+ await storage.createRoom({ name: 'room2', clientIds: [] });
107
+
108
+ const rooms = await storage.getAllRooms();
109
+ expect(rooms).toHaveLength(2);
110
+ });
111
+
112
+ it('should get rooms by pattern', async () => {
113
+ await storage.createRoom({ name: 'chat:general', clientIds: [] });
114
+ await storage.createRoom({ name: 'chat:private', clientIds: [] });
115
+ await storage.createRoom({ name: 'game:lobby', clientIds: [] });
116
+
117
+ const chatRooms = await storage.getRoomsByPattern('chat:*');
118
+ expect(chatRooms).toHaveLength(2);
119
+ });
120
+
121
+ it('should update room metadata', async () => {
122
+ await storage.createRoom({ name: 'test-room', clientIds: [] });
123
+
124
+ await storage.updateRoomMetadata('test-room', { topic: 'General Discussion' });
125
+
126
+ const room = await storage.getRoom('test-room');
127
+ expect(room?.metadata).toEqual({ topic: 'General Discussion' });
128
+ });
129
+ });
130
+
131
+ describe('room membership', () => {
132
+ const createClient = (id: string): WsClientData => ({
133
+ id,
134
+ rooms: [],
135
+ connectedAt: Date.now(),
136
+ auth: null,
137
+ metadata: {},
138
+ });
139
+
140
+ it('should add client to room', async () => {
141
+ await storage.addClient(createClient('client1'));
142
+ await storage.addClientToRoom('client1', 'test-room');
143
+
144
+ const clientIds = await storage.getClientsInRoom('test-room');
145
+ expect(clientIds).toContain('client1');
146
+ });
147
+
148
+ it('should create room automatically when adding client', async () => {
149
+ await storage.addClient(createClient('client1'));
150
+ await storage.addClientToRoom('client1', 'auto-room');
151
+
152
+ const room = await storage.getRoom('auto-room');
153
+ expect(room).not.toBeNull();
154
+ });
155
+
156
+ it('should update client rooms list', async () => {
157
+ await storage.addClient(createClient('client1'));
158
+ await storage.addClientToRoom('client1', 'room1');
159
+ await storage.addClientToRoom('client1', 'room2');
160
+
161
+ const client = await storage.getClient('client1');
162
+ expect(client?.rooms).toContain('room1');
163
+ expect(client?.rooms).toContain('room2');
164
+ });
165
+
166
+ it('should remove client from room', async () => {
167
+ await storage.addClient(createClient('client1'));
168
+ await storage.addClientToRoom('client1', 'test-room');
169
+
170
+ await storage.removeClientFromRoom('client1', 'test-room');
171
+
172
+ const clientIds = await storage.getClientsInRoom('test-room');
173
+ expect(clientIds).not.toContain('client1');
174
+ });
175
+
176
+ it('should delete empty room after client leaves', async () => {
177
+ await storage.addClient(createClient('client1'));
178
+ await storage.addClientToRoom('client1', 'temp-room');
179
+
180
+ await storage.removeClientFromRoom('client1', 'temp-room');
181
+
182
+ const room = await storage.getRoom('temp-room');
183
+ expect(room).toBeNull();
184
+ });
185
+
186
+ it('should get rooms for a client', async () => {
187
+ await storage.addClient(createClient('client1'));
188
+ await storage.addClientToRoom('client1', 'room1');
189
+ await storage.addClientToRoom('client1', 'room2');
190
+
191
+ const rooms = await storage.getRoomsForClient('client1');
192
+ expect(rooms).toHaveLength(2);
193
+ expect(rooms).toContain('room1');
194
+ expect(rooms).toContain('room2');
195
+ });
196
+
197
+ it('should remove client from all rooms', async () => {
198
+ await storage.addClient(createClient('client1'));
199
+ await storage.addClientToRoom('client1', 'room1');
200
+ await storage.addClientToRoom('client1', 'room2');
201
+
202
+ await storage.removeClientFromAllRooms('client1');
203
+
204
+ const rooms = await storage.getRoomsForClient('client1');
205
+ expect(rooms).toHaveLength(0);
206
+ });
207
+
208
+ it('should remove client from all rooms when client is removed', async () => {
209
+ await storage.addClient(createClient('client1'));
210
+ await storage.addClient(createClient('client2'));
211
+ await storage.addClientToRoom('client1', 'shared-room');
212
+ await storage.addClientToRoom('client2', 'shared-room');
213
+
214
+ await storage.removeClient('client1');
215
+
216
+ const clientIds = await storage.getClientsInRoom('shared-room');
217
+ expect(clientIds).not.toContain('client1');
218
+ expect(clientIds).toContain('client2');
219
+ });
220
+ });
221
+
222
+ describe('lifecycle', () => {
223
+ it('should clear all data', async () => {
224
+ const client: WsClientData = {
225
+ id: 'client1',
226
+ rooms: [],
227
+ connectedAt: Date.now(),
228
+ auth: null,
229
+ metadata: {},
230
+ };
231
+ await storage.addClient(client);
232
+ await storage.createRoom({ name: 'test-room', clientIds: [] });
233
+
234
+ await storage.clear();
235
+
236
+ expect(await storage.getClientCount()).toBe(0);
237
+ expect(await storage.getAllRooms()).toHaveLength(0);
238
+ });
239
+
240
+ it('should return stats', () => {
241
+ const stats = storage.getStats();
242
+ expect(stats).toHaveProperty('clientCount');
243
+ expect(stats).toHaveProperty('roomCount');
244
+ });
245
+ });
246
+ });
@@ -0,0 +1,222 @@
1
+ /**
2
+ * In-Memory WebSocket Storage
3
+ *
4
+ * Simple in-memory implementation of WsStorageAdapter.
5
+ * Suitable for single-instance deployments.
6
+ */
7
+
8
+ import type { WsStorageAdapter } from './ws-storage';
9
+ import type { WsClientData, WsRoom } from './ws.types';
10
+
11
+ import { isPatternMatch } from './ws-pattern-matcher';
12
+
13
+ /**
14
+ * In-memory storage for WebSocket clients and rooms
15
+ */
16
+ export class InMemoryWsStorage implements WsStorageAdapter {
17
+ private clients = new Map<string, WsClientData>();
18
+ private rooms = new Map<string, WsRoom>();
19
+
20
+ // ============================================================================
21
+ // Client Operations
22
+ // ============================================================================
23
+
24
+ async addClient(client: WsClientData): Promise<void> {
25
+ this.clients.set(client.id, { ...client });
26
+ }
27
+
28
+ async removeClient(clientId: string): Promise<void> {
29
+ // Remove from all rooms first
30
+ await this.removeClientFromAllRooms(clientId);
31
+ // Then remove the client
32
+ this.clients.delete(clientId);
33
+ }
34
+
35
+ async getClient(clientId: string): Promise<WsClientData | null> {
36
+ const client = this.clients.get(clientId);
37
+
38
+ return client ? { ...client } : null;
39
+ }
40
+
41
+ async getAllClients(): Promise<WsClientData[]> {
42
+ return Array.from(this.clients.values()).map((c) => ({ ...c }));
43
+ }
44
+
45
+ async updateClient(clientId: string, data: Partial<WsClientData>): Promise<void> {
46
+ const client = this.clients.get(clientId);
47
+ if (client) {
48
+ this.clients.set(clientId, { ...client, ...data });
49
+ }
50
+ }
51
+
52
+ async getClientCount(): Promise<number> {
53
+ return this.clients.size;
54
+ }
55
+
56
+ // ============================================================================
57
+ // Room Operations
58
+ // ============================================================================
59
+
60
+ async createRoom(room: WsRoom): Promise<void> {
61
+ this.rooms.set(room.name, { ...room, clientIds: [...room.clientIds] });
62
+ }
63
+
64
+ async deleteRoom(name: string): Promise<void> {
65
+ const room = this.rooms.get(name);
66
+ if (room) {
67
+ // Remove room from all clients' room lists
68
+ for (const clientId of room.clientIds) {
69
+ const client = this.clients.get(clientId);
70
+ if (client) {
71
+ client.rooms = client.rooms.filter((r) => r !== name);
72
+ }
73
+ }
74
+ this.rooms.delete(name);
75
+ }
76
+ }
77
+
78
+ async getRoom(name: string): Promise<WsRoom | null> {
79
+ const room = this.rooms.get(name);
80
+
81
+ return room ? { ...room, clientIds: [...room.clientIds] } : null;
82
+ }
83
+
84
+ async getAllRooms(): Promise<WsRoom[]> {
85
+ return Array.from(this.rooms.values()).map((r) => ({
86
+ ...r,
87
+ clientIds: [...r.clientIds],
88
+ }));
89
+ }
90
+
91
+ async getRoomsByPattern(pattern: string): Promise<WsRoom[]> {
92
+ const matchingRooms: WsRoom[] = [];
93
+
94
+ for (const [name, room] of this.rooms) {
95
+ if (isPatternMatch(pattern, name)) {
96
+ matchingRooms.push({ ...room, clientIds: [...room.clientIds] });
97
+ }
98
+ }
99
+
100
+ return matchingRooms;
101
+ }
102
+
103
+ async updateRoomMetadata(name: string, metadata: Record<string, unknown>): Promise<void> {
104
+ const room = this.rooms.get(name);
105
+ if (room) {
106
+ room.metadata = { ...room.metadata, ...metadata };
107
+ }
108
+ }
109
+
110
+ // ============================================================================
111
+ // Room Membership Operations
112
+ // ============================================================================
113
+
114
+ async addClientToRoom(clientId: string, roomName: string): Promise<void> {
115
+ // Get or create room
116
+ let room = this.rooms.get(roomName);
117
+ if (!room) {
118
+ room = { name: roomName, clientIds: [] };
119
+ this.rooms.set(roomName, room);
120
+ }
121
+
122
+ // Add client to room if not already there
123
+ if (!room.clientIds.includes(clientId)) {
124
+ room.clientIds.push(clientId);
125
+ }
126
+
127
+ // Update client's room list
128
+ const client = this.clients.get(clientId);
129
+ if (client && !client.rooms.includes(roomName)) {
130
+ client.rooms.push(roomName);
131
+ }
132
+ }
133
+
134
+ async removeClientFromRoom(clientId: string, roomName: string): Promise<void> {
135
+ // Remove from room
136
+ const room = this.rooms.get(roomName);
137
+ if (room) {
138
+ room.clientIds = room.clientIds.filter((id) => id !== clientId);
139
+
140
+ // Delete room if empty
141
+ if (room.clientIds.length === 0) {
142
+ this.rooms.delete(roomName);
143
+ }
144
+ }
145
+
146
+ // Update client's room list
147
+ const client = this.clients.get(clientId);
148
+ if (client) {
149
+ client.rooms = client.rooms.filter((r) => r !== roomName);
150
+ }
151
+ }
152
+
153
+ async getClientsInRoom(roomName: string): Promise<string[]> {
154
+ const room = this.rooms.get(roomName);
155
+
156
+ return room ? [...room.clientIds] : [];
157
+ }
158
+
159
+ async getRoomsForClient(clientId: string): Promise<string[]> {
160
+ const client = this.clients.get(clientId);
161
+
162
+ return client ? [...client.rooms] : [];
163
+ }
164
+
165
+ async removeClientFromAllRooms(clientId: string): Promise<void> {
166
+ const client = this.clients.get(clientId);
167
+ if (!client) {
168
+ return;
169
+ }
170
+
171
+ // Remove from all rooms
172
+ for (const roomName of client.rooms) {
173
+ const room = this.rooms.get(roomName);
174
+ if (room) {
175
+ room.clientIds = room.clientIds.filter((id) => id !== clientId);
176
+
177
+ // Delete room if empty
178
+ if (room.clientIds.length === 0) {
179
+ this.rooms.delete(roomName);
180
+ }
181
+ }
182
+ }
183
+
184
+ // Clear client's room list
185
+ client.rooms = [];
186
+ }
187
+
188
+ // ============================================================================
189
+ // Lifecycle Operations
190
+ // ============================================================================
191
+
192
+ async clear(): Promise<void> {
193
+ this.clients.clear();
194
+ this.rooms.clear();
195
+ }
196
+
197
+ async close(): Promise<void> {
198
+ // No-op for in-memory storage
199
+ await this.clear();
200
+ }
201
+
202
+ // ============================================================================
203
+ // Debug/Stats Methods
204
+ // ============================================================================
205
+
206
+ /**
207
+ * Get storage statistics (for debugging)
208
+ */
209
+ getStats(): { clientCount: number; roomCount: number } {
210
+ return {
211
+ clientCount: this.clients.size,
212
+ roomCount: this.rooms.size,
213
+ };
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Create a new in-memory WebSocket storage instance
219
+ */
220
+ export function createInMemoryWsStorage(): WsStorageAdapter {
221
+ return new InMemoryWsStorage();
222
+ }