@principal-ai/control-tower-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.
Files changed (84) hide show
  1. package/README.md +84 -0
  2. package/dist/abstractions/AuthAdapter.d.ts +9 -0
  3. package/dist/abstractions/AuthAdapter.d.ts.map +1 -0
  4. package/dist/abstractions/AuthAdapter.js +1 -0
  5. package/dist/abstractions/DefaultLockManager.d.ts +18 -0
  6. package/dist/abstractions/DefaultLockManager.d.ts.map +1 -0
  7. package/dist/abstractions/DefaultLockManager.js +152 -0
  8. package/dist/abstractions/DefaultRoomManager.d.ts +15 -0
  9. package/dist/abstractions/DefaultRoomManager.d.ts.map +1 -0
  10. package/dist/abstractions/DefaultRoomManager.js +91 -0
  11. package/dist/abstractions/EventEmitter.d.ts +14 -0
  12. package/dist/abstractions/EventEmitter.d.ts.map +1 -0
  13. package/dist/abstractions/EventEmitter.js +56 -0
  14. package/dist/abstractions/LockManager.d.ts +16 -0
  15. package/dist/abstractions/LockManager.d.ts.map +1 -0
  16. package/dist/abstractions/LockManager.js +9 -0
  17. package/dist/abstractions/RoomManager.d.ts +15 -0
  18. package/dist/abstractions/RoomManager.d.ts.map +1 -0
  19. package/dist/abstractions/RoomManager.js +5 -0
  20. package/dist/abstractions/StorageAdapter.d.ts +25 -0
  21. package/dist/abstractions/StorageAdapter.d.ts.map +1 -0
  22. package/dist/abstractions/StorageAdapter.js +1 -0
  23. package/dist/abstractions/TransportAdapter.d.ts +17 -0
  24. package/dist/abstractions/TransportAdapter.d.ts.map +1 -0
  25. package/dist/abstractions/TransportAdapter.js +1 -0
  26. package/dist/abstractions/index.d.ts +9 -0
  27. package/dist/abstractions/index.d.ts.map +1 -0
  28. package/dist/abstractions/index.js +5 -0
  29. package/dist/adapters/mock/MockAuthAdapter.d.ts +27 -0
  30. package/dist/adapters/mock/MockAuthAdapter.d.ts.map +1 -0
  31. package/dist/adapters/mock/MockAuthAdapter.js +161 -0
  32. package/dist/adapters/mock/MockStorageAdapter.d.ts +27 -0
  33. package/dist/adapters/mock/MockStorageAdapter.d.ts.map +1 -0
  34. package/dist/adapters/mock/MockStorageAdapter.js +162 -0
  35. package/dist/adapters/mock/MockTransportAdapter.d.ts +31 -0
  36. package/dist/adapters/mock/MockTransportAdapter.d.ts.map +1 -0
  37. package/dist/adapters/mock/MockTransportAdapter.js +90 -0
  38. package/dist/adapters/mock/index.d.ts +4 -0
  39. package/dist/adapters/mock/index.d.ts.map +1 -0
  40. package/dist/adapters/mock/index.js +3 -0
  41. package/dist/adapters/websocket/WebSocketTransportAdapter.d.ts +40 -0
  42. package/dist/adapters/websocket/WebSocketTransportAdapter.d.ts.map +1 -0
  43. package/dist/adapters/websocket/WebSocketTransportAdapter.js +204 -0
  44. package/dist/adapters/websocket/index.d.ts +2 -0
  45. package/dist/adapters/websocket/index.d.ts.map +1 -0
  46. package/dist/adapters/websocket/index.js +1 -0
  47. package/dist/client/BaseClient.d.ts +103 -0
  48. package/dist/client/BaseClient.d.ts.map +1 -0
  49. package/dist/client/BaseClient.js +282 -0
  50. package/dist/client/ClientBuilder.d.ts +16 -0
  51. package/dist/client/ClientBuilder.d.ts.map +1 -0
  52. package/dist/client/ClientBuilder.js +37 -0
  53. package/dist/client/index.d.ts +3 -0
  54. package/dist/client/index.d.ts.map +1 -0
  55. package/dist/client/index.js +3 -0
  56. package/dist/index.d.ts +7 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +9 -1205
  59. package/dist/index.js.map +23 -6
  60. package/dist/server/BaseServer.d.ts +116 -0
  61. package/dist/server/BaseServer.d.ts.map +1 -0
  62. package/dist/server/BaseServer.js +422 -0
  63. package/dist/server/ServerBuilder.d.ts +32 -0
  64. package/dist/server/ServerBuilder.d.ts.map +1 -0
  65. package/dist/server/ServerBuilder.js +66 -0
  66. package/dist/server/index.d.ts +3 -0
  67. package/dist/server/index.d.ts.map +1 -0
  68. package/dist/server/index.js +3 -0
  69. package/dist/types/auth.d.ts +40 -0
  70. package/dist/types/auth.d.ts.map +1 -0
  71. package/dist/types/auth.js +1 -0
  72. package/dist/types/events.d.ts +66 -0
  73. package/dist/types/events.d.ts.map +1 -0
  74. package/dist/types/events.js +1 -0
  75. package/dist/types/index.d.ts +23 -0
  76. package/dist/types/index.d.ts.map +1 -0
  77. package/dist/types/index.js +1 -0
  78. package/dist/types/lock.d.ts +35 -0
  79. package/dist/types/lock.d.ts.map +1 -0
  80. package/dist/types/lock.js +1 -0
  81. package/dist/types/room.d.ts +40 -0
  82. package/dist/types/room.d.ts.map +1 -0
  83. package/dist/types/room.js +1 -0
  84. package/package.json +7 -3
@@ -0,0 +1,204 @@
1
+ import { WebSocketServer, WebSocket } from 'ws';
2
+ export class WebSocketTransportAdapter {
3
+ constructor() {
4
+ this.state = 'disconnected';
5
+ this.messageHandlers = new Set();
6
+ this.errorHandlers = new Set();
7
+ this.closeHandlers = new Set();
8
+ // Client management
9
+ this.clients = new Map();
10
+ this.mode = 'standalone';
11
+ }
12
+ // Standalone mode implementation
13
+ async connect(url, _options) {
14
+ if (this.state === 'connected' || this.state === 'connecting') {
15
+ throw new Error('Already connected or connecting');
16
+ }
17
+ this.state = 'connecting';
18
+ this.serverUrl = url;
19
+ this.mode = 'standalone';
20
+ try {
21
+ // Extract port from URL
22
+ const urlObj = new URL(url);
23
+ const port = parseInt(urlObj.port) || (urlObj.protocol === 'wss:' ? 443 : 80);
24
+ // Create WebSocket server
25
+ this.wss = new WebSocketServer({ port });
26
+ this.wss.on('connection', (ws, req) => {
27
+ this.handleConnection(ws, req);
28
+ });
29
+ this.wss.on('error', (error) => {
30
+ this.errorHandlers.forEach(handler => handler(error));
31
+ });
32
+ this.wss.on('close', () => {
33
+ this.state = 'disconnected';
34
+ this.closeHandlers.forEach(handler => handler(1000, 'Server closed'));
35
+ });
36
+ this.state = 'connected';
37
+ }
38
+ catch (error) {
39
+ this.state = 'disconnected';
40
+ throw error;
41
+ }
42
+ }
43
+ // Integration mode implementation
44
+ async attach(server, path = '/ws') {
45
+ if (this.state === 'connected' || this.state === 'connecting') {
46
+ throw new Error('Already connected or connecting');
47
+ }
48
+ this.state = 'connecting';
49
+ this.attachedServer = server;
50
+ this.webSocketPath = path;
51
+ this.mode = 'integration';
52
+ try {
53
+ // Create WebSocket server attached to the existing HTTP server
54
+ this.wss = new WebSocketServer({
55
+ server,
56
+ path,
57
+ perMessageDeflate: false
58
+ });
59
+ this.wss.on('connection', (ws, req) => {
60
+ this.handleConnection(ws, req);
61
+ });
62
+ this.wss.on('error', (error) => {
63
+ this.errorHandlers.forEach(handler => handler(error));
64
+ });
65
+ this.state = 'connected';
66
+ }
67
+ catch (error) {
68
+ this.state = 'disconnected';
69
+ throw error;
70
+ }
71
+ }
72
+ async attachToWebSocketServer(wss) {
73
+ if (this.state === 'connected' || this.state === 'connecting') {
74
+ throw new Error('Already connected or connecting');
75
+ }
76
+ this.state = 'connecting';
77
+ this.attachedWss = wss;
78
+ this.mode = 'integration';
79
+ try {
80
+ this.wss = wss;
81
+ this.wss.on('connection', (ws, req) => {
82
+ this.handleConnection(ws, req);
83
+ });
84
+ this.wss.on('error', (error) => {
85
+ this.errorHandlers.forEach(handler => handler(error));
86
+ });
87
+ this.state = 'connected';
88
+ }
89
+ catch (error) {
90
+ this.state = 'disconnected';
91
+ throw error;
92
+ }
93
+ }
94
+ handleConnection(ws, req) {
95
+ const clientId = this.generateId();
96
+ const client = {
97
+ id: clientId,
98
+ ws,
99
+ userId: typeof req.headers['x-user-id'] === 'string' ? req.headers['x-user-id'] : undefined
100
+ };
101
+ this.clients.set(clientId, client);
102
+ ws.on('message', (data) => {
103
+ try {
104
+ const message = JSON.parse(data.toString());
105
+ // Add clientId to message payload for routing
106
+ const enrichedMessage = {
107
+ ...message,
108
+ payload: {
109
+ ...message.payload,
110
+ clientId
111
+ }
112
+ };
113
+ this.messageHandlers.forEach(handler => handler(enrichedMessage));
114
+ }
115
+ catch (error) {
116
+ this.errorHandlers.forEach(handler => handler(error));
117
+ }
118
+ });
119
+ ws.on('error', (error) => {
120
+ this.errorHandlers.forEach(handler => handler(error));
121
+ });
122
+ ws.on('close', (_code, _reason) => {
123
+ this.clients.delete(clientId);
124
+ // Don't emit close for individual clients, only for server shutdown
125
+ });
126
+ }
127
+ async disconnect() {
128
+ if (this.state === 'disconnected') {
129
+ return;
130
+ }
131
+ this.state = 'disconnecting';
132
+ // Close all client connections
133
+ for (const [_clientId, client] of this.clients) {
134
+ try {
135
+ client.ws.close(1000, 'Server shutting down');
136
+ }
137
+ catch {
138
+ // Ignore errors when closing individual connections
139
+ }
140
+ }
141
+ this.clients.clear();
142
+ // Close the WebSocket server (only if we created it)
143
+ if (this.wss && this.mode === 'standalone') {
144
+ await new Promise((resolve) => {
145
+ this.wss.close(() => resolve());
146
+ });
147
+ }
148
+ this.state = 'disconnected';
149
+ this.wss = undefined;
150
+ this.attachedServer = undefined;
151
+ this.attachedWss = undefined;
152
+ }
153
+ async send(message) {
154
+ if (this.state !== 'connected') {
155
+ throw new Error('Not connected');
156
+ }
157
+ // Extract clientId from message payload for routing
158
+ const payload = message.payload;
159
+ const { clientId, ...clientMessage } = payload;
160
+ if (!clientId) {
161
+ throw new Error('Message must contain clientId in payload for routing');
162
+ }
163
+ const client = this.clients.get(clientId);
164
+ if (!client) {
165
+ throw new Error(`Client ${clientId} not found`);
166
+ }
167
+ if (client.ws.readyState !== WebSocket.OPEN) {
168
+ throw new Error(`Client ${clientId} connection is not open`);
169
+ }
170
+ const messageToSend = {
171
+ ...message,
172
+ payload: clientMessage
173
+ };
174
+ client.ws.send(JSON.stringify(messageToSend));
175
+ }
176
+ onMessage(handler) {
177
+ this.messageHandlers.add(handler);
178
+ }
179
+ onError(handler) {
180
+ this.errorHandlers.add(handler);
181
+ }
182
+ onClose(handler) {
183
+ this.closeHandlers.add(handler);
184
+ }
185
+ getState() {
186
+ return this.state;
187
+ }
188
+ isConnected() {
189
+ return this.state === 'connected';
190
+ }
191
+ // Utility methods
192
+ getConnectedClients() {
193
+ return Array.from(this.clients.values());
194
+ }
195
+ getClientCount() {
196
+ return this.clients.size;
197
+ }
198
+ getMode() {
199
+ return this.mode;
200
+ }
201
+ generateId() {
202
+ return Math.random().toString(36).substring(2) + Date.now().toString(36);
203
+ }
204
+ }
@@ -0,0 +1,2 @@
1
+ export { WebSocketTransportAdapter } from './WebSocketTransportAdapter';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/adapters/websocket/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAC"}
@@ -0,0 +1 @@
1
+ export { WebSocketTransportAdapter } from './WebSocketTransportAdapter';
@@ -0,0 +1,103 @@
1
+ import type { ConnectionState, Event, RoomState, RoomUser, LockRequest, Lock, Credentials } from '../types';
2
+ import type { ITransportAdapter } from '../abstractions/TransportAdapter';
3
+ import type { IAuthAdapter } from '../abstractions/AuthAdapter';
4
+ import type { IStorageAdapter } from '../abstractions/StorageAdapter';
5
+ import { TypedEventEmitter } from '../abstractions/EventEmitter';
6
+ export interface ClientConfig {
7
+ transport: ITransportAdapter;
8
+ auth?: IAuthAdapter;
9
+ storage?: IStorageAdapter;
10
+ reconnection?: {
11
+ enabled: boolean;
12
+ maxAttempts: number;
13
+ initialDelay: number;
14
+ maxDelay: number;
15
+ backoffFactor: number;
16
+ };
17
+ }
18
+ export interface ClientEvents {
19
+ connected: {
20
+ url: string;
21
+ };
22
+ disconnected: {
23
+ code: number;
24
+ reason: string;
25
+ };
26
+ reconnecting: {
27
+ attempt: number;
28
+ delay: number;
29
+ };
30
+ reconnected: {
31
+ url: string;
32
+ };
33
+ error: {
34
+ error: Error;
35
+ };
36
+ room_joined: {
37
+ roomId: string;
38
+ state: RoomState;
39
+ };
40
+ room_left: {
41
+ roomId: string;
42
+ };
43
+ event_received: {
44
+ event: Event;
45
+ };
46
+ lock_acquired: {
47
+ lock: Lock;
48
+ };
49
+ lock_released: {
50
+ lockId: string;
51
+ };
52
+ lock_denied: {
53
+ request: LockRequest;
54
+ reason: string;
55
+ };
56
+ presence_updated: {
57
+ users: RoomUser[];
58
+ };
59
+ [key: string]: unknown;
60
+ }
61
+ export declare class BaseClient extends TypedEventEmitter<ClientEvents> {
62
+ private transport;
63
+ private auth?;
64
+ private storage?;
65
+ private config;
66
+ private connectionState;
67
+ private currentRoomId;
68
+ private currentRoomState;
69
+ private authToken;
70
+ private userId;
71
+ private reconnectAttempts;
72
+ private reconnectTimer;
73
+ private lastConnectionUrl;
74
+ private lastCredentials;
75
+ constructor(config: ClientConfig);
76
+ connect(url: string, credentials?: Credentials): Promise<void>;
77
+ disconnect(): Promise<void>;
78
+ reconnect(): Promise<void>;
79
+ joinRoom(roomId: string): Promise<void>;
80
+ leaveRoom(): Promise<void>;
81
+ broadcast(event: Event): Promise<void>;
82
+ requestLock(request: LockRequest): Promise<void>;
83
+ releaseLock(lockId: string): Promise<void>;
84
+ getConnectionState(): ConnectionState;
85
+ getRoomState(): RoomState | null;
86
+ getPresence(): RoomUser[];
87
+ getCurrentRoomId(): string | null;
88
+ getUserId(): string | null;
89
+ private handleMessage;
90
+ private handleError;
91
+ private handleClose;
92
+ private handleRoomJoined;
93
+ private handleRoomLeft;
94
+ private handleEventBroadcast;
95
+ private handleLockAcquired;
96
+ private handleLockReleased;
97
+ private handleLockDenied;
98
+ private handlePresenceUpdated;
99
+ private handleServerError;
100
+ private scheduleReconnect;
101
+ private generateId;
102
+ }
103
+ //# sourceMappingURL=BaseClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BaseClient.d.ts","sourceRoot":"","sources":["../../src/client/BaseClient.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,282 @@
1
+ import { TypedEventEmitter } from '../abstractions/EventEmitter';
2
+ export class BaseClient extends TypedEventEmitter {
3
+ constructor(config) {
4
+ super();
5
+ this.connectionState = 'disconnected';
6
+ this.currentRoomId = null;
7
+ this.currentRoomState = null;
8
+ this.authToken = null;
9
+ this.userId = null;
10
+ // Reconnection state
11
+ this.reconnectAttempts = 0;
12
+ this.reconnectTimer = null;
13
+ this.lastConnectionUrl = null;
14
+ this.lastCredentials = null;
15
+ this.config = config;
16
+ this.transport = config.transport;
17
+ this.auth = config.auth;
18
+ this.storage = config.storage;
19
+ // Set up transport event handlers
20
+ this.transport.onMessage(this.handleMessage.bind(this));
21
+ this.transport.onError(this.handleError.bind(this));
22
+ this.transport.onClose(this.handleClose.bind(this));
23
+ }
24
+ // Connection Management
25
+ async connect(url, credentials) {
26
+ if (this.connectionState !== 'disconnected') {
27
+ throw new Error('Client is already connected or connecting');
28
+ }
29
+ this.connectionState = 'connecting';
30
+ this.lastConnectionUrl = url;
31
+ this.lastCredentials = credentials || null;
32
+ try {
33
+ // Authenticate if credentials provided and auth adapter available
34
+ if (credentials && this.auth) {
35
+ const authResult = await this.auth.authenticate(credentials);
36
+ this.authToken = authResult.token;
37
+ this.userId = authResult.user.userId;
38
+ }
39
+ const options = {
40
+ url,
41
+ reconnect: false, // We handle reconnection ourselves
42
+ ...this.config.reconnection
43
+ };
44
+ await this.transport.connect(url, options);
45
+ this.connectionState = 'connected';
46
+ await this.emit('connected', { url });
47
+ }
48
+ catch (error) {
49
+ this.connectionState = 'disconnected';
50
+ await this.emit('error', { error: error });
51
+ throw error;
52
+ }
53
+ }
54
+ async disconnect() {
55
+ if (this.connectionState === 'disconnected') {
56
+ return;
57
+ }
58
+ this.connectionState = 'disconnecting';
59
+ // Clear reconnection timer
60
+ if (this.reconnectTimer) {
61
+ clearTimeout(this.reconnectTimer);
62
+ this.reconnectTimer = null;
63
+ }
64
+ // Leave room if joined
65
+ if (this.currentRoomId) {
66
+ await this.leaveRoom();
67
+ }
68
+ try {
69
+ await this.transport.disconnect();
70
+ }
71
+ finally {
72
+ this.connectionState = 'disconnected';
73
+ this.currentRoomId = null;
74
+ this.currentRoomState = null;
75
+ this.authToken = null;
76
+ this.userId = null;
77
+ this.reconnectAttempts = 0;
78
+ await this.emit('disconnected', { code: 1000, reason: 'Client disconnected' });
79
+ }
80
+ }
81
+ async reconnect() {
82
+ if (!this.lastConnectionUrl) {
83
+ throw new Error('No previous connection to reconnect to');
84
+ }
85
+ if (this.connectionState !== 'disconnected') {
86
+ await this.disconnect();
87
+ }
88
+ await this.connect(this.lastConnectionUrl, this.lastCredentials || undefined);
89
+ }
90
+ // Room Management
91
+ async joinRoom(roomId) {
92
+ if (this.connectionState !== 'connected') {
93
+ throw new Error('Client must be connected to join a room');
94
+ }
95
+ if (this.currentRoomId) {
96
+ await this.leaveRoom();
97
+ }
98
+ const message = {
99
+ id: this.generateId(),
100
+ type: 'join_room',
101
+ payload: { roomId, token: this.authToken },
102
+ timestamp: Date.now()
103
+ };
104
+ await this.transport.send(message);
105
+ // Response will be handled in handleMessage
106
+ }
107
+ async leaveRoom() {
108
+ if (!this.currentRoomId) {
109
+ return;
110
+ }
111
+ const message = {
112
+ id: this.generateId(),
113
+ type: 'leave_room',
114
+ payload: { roomId: this.currentRoomId },
115
+ timestamp: Date.now()
116
+ };
117
+ await this.transport.send(message);
118
+ // Response will be handled in handleMessage
119
+ this.currentRoomId = null;
120
+ this.currentRoomState = null;
121
+ }
122
+ // Event Operations
123
+ async broadcast(event) {
124
+ if (this.connectionState !== 'connected' || !this.currentRoomId) {
125
+ throw new Error('Client must be connected and in a room to broadcast events');
126
+ }
127
+ const message = {
128
+ id: this.generateId(),
129
+ type: 'broadcast_event',
130
+ payload: { event, roomId: this.currentRoomId },
131
+ timestamp: Date.now()
132
+ };
133
+ await this.transport.send(message);
134
+ }
135
+ // Lock Operations
136
+ async requestLock(request) {
137
+ if (this.connectionState !== 'connected' || !this.currentRoomId) {
138
+ throw new Error('Client must be connected and in a room to request locks');
139
+ }
140
+ const message = {
141
+ id: this.generateId(),
142
+ type: 'request_lock',
143
+ payload: { request, roomId: this.currentRoomId },
144
+ timestamp: Date.now()
145
+ };
146
+ await this.transport.send(message);
147
+ }
148
+ async releaseLock(lockId) {
149
+ if (this.connectionState !== 'connected' || !this.currentRoomId) {
150
+ throw new Error('Client must be connected and in a room to release locks');
151
+ }
152
+ const message = {
153
+ id: this.generateId(),
154
+ type: 'release_lock',
155
+ payload: { lockId, roomId: this.currentRoomId },
156
+ timestamp: Date.now()
157
+ };
158
+ await this.transport.send(message);
159
+ }
160
+ // State Getters
161
+ getConnectionState() {
162
+ return this.connectionState;
163
+ }
164
+ getRoomState() {
165
+ return this.currentRoomState;
166
+ }
167
+ getPresence() {
168
+ return this.currentRoomState ? Array.from(this.currentRoomState.users.values()) : [];
169
+ }
170
+ getCurrentRoomId() {
171
+ return this.currentRoomId;
172
+ }
173
+ getUserId() {
174
+ return this.userId;
175
+ }
176
+ // Private Methods
177
+ async handleMessage(message) {
178
+ try {
179
+ switch (message.type) {
180
+ case 'room_joined':
181
+ await this.handleRoomJoined(message.payload);
182
+ break;
183
+ case 'room_left':
184
+ await this.handleRoomLeft(message.payload);
185
+ break;
186
+ case 'event_broadcast':
187
+ await this.handleEventBroadcast(message.payload);
188
+ break;
189
+ case 'lock_acquired':
190
+ await this.handleLockAcquired(message.payload);
191
+ break;
192
+ case 'lock_released':
193
+ await this.handleLockReleased(message.payload);
194
+ break;
195
+ case 'lock_denied':
196
+ await this.handleLockDenied(message.payload);
197
+ break;
198
+ case 'presence_updated':
199
+ await this.handlePresenceUpdated(message.payload);
200
+ break;
201
+ case 'error':
202
+ await this.handleServerError(message.payload);
203
+ break;
204
+ }
205
+ }
206
+ catch (error) {
207
+ await this.emit('error', { error: error });
208
+ // Error is already emitted, no additional handling needed
209
+ }
210
+ }
211
+ async handleError(error) {
212
+ await this.emit('error', { error });
213
+ }
214
+ async handleClose(code, reason) {
215
+ const wasConnected = this.connectionState === 'connected';
216
+ this.connectionState = 'disconnected';
217
+ if (wasConnected) {
218
+ await this.emit('disconnected', { code, reason });
219
+ // Attempt reconnection if enabled
220
+ if (this.config.reconnection?.enabled && this.lastConnectionUrl) {
221
+ this.scheduleReconnect();
222
+ }
223
+ }
224
+ }
225
+ async handleRoomJoined(payload) {
226
+ this.currentRoomId = payload.roomId;
227
+ this.currentRoomState = payload.state;
228
+ await this.emit('room_joined', payload);
229
+ }
230
+ async handleRoomLeft(payload) {
231
+ if (this.currentRoomId === payload.roomId) {
232
+ this.currentRoomId = null;
233
+ this.currentRoomState = null;
234
+ }
235
+ await this.emit('room_left', payload);
236
+ }
237
+ async handleEventBroadcast(payload) {
238
+ await this.emit('event_received', payload);
239
+ }
240
+ async handleLockAcquired(payload) {
241
+ await this.emit('lock_acquired', payload);
242
+ }
243
+ async handleLockReleased(payload) {
244
+ await this.emit('lock_released', payload);
245
+ }
246
+ async handleLockDenied(payload) {
247
+ await this.emit('lock_denied', payload);
248
+ }
249
+ async handlePresenceUpdated(payload) {
250
+ if (this.currentRoomState) {
251
+ // Convert array back to Map
252
+ this.currentRoomState.users = new Map(payload.users.map(user => [user.id, user]));
253
+ }
254
+ await this.emit('presence_updated', payload);
255
+ }
256
+ async handleServerError(payload) {
257
+ const error = new Error(payload.error);
258
+ await this.emit('error', { error });
259
+ }
260
+ async scheduleReconnect() {
261
+ if (!this.config.reconnection || this.reconnectAttempts >= this.config.reconnection.maxAttempts) {
262
+ return;
263
+ }
264
+ const delay = Math.min(this.config.reconnection.initialDelay * Math.pow(this.config.reconnection.backoffFactor, this.reconnectAttempts), this.config.reconnection.maxDelay);
265
+ this.reconnectAttempts++;
266
+ await this.emit('reconnecting', { attempt: this.reconnectAttempts, delay });
267
+ this.reconnectTimer = setTimeout(async () => {
268
+ try {
269
+ await this.reconnect();
270
+ this.reconnectAttempts = 0;
271
+ await this.emit('reconnected', { url: this.lastConnectionUrl });
272
+ }
273
+ catch {
274
+ // Reconnection failed, schedule next attempt
275
+ this.scheduleReconnect();
276
+ }
277
+ }, delay);
278
+ }
279
+ generateId() {
280
+ return Math.random().toString(36).substring(2) + Date.now().toString(36);
281
+ }
282
+ }
@@ -0,0 +1,16 @@
1
+ import type { ITransportAdapter } from '../abstractions/TransportAdapter';
2
+ import type { IAuthAdapter } from '../abstractions/AuthAdapter';
3
+ import type { IStorageAdapter } from '../abstractions/StorageAdapter';
4
+ import { BaseClient, type ClientConfig } from './BaseClient';
5
+ export declare class ClientBuilder {
6
+ private transport?;
7
+ private auth?;
8
+ private storage?;
9
+ private reconnection?;
10
+ withTransport(transport: ITransportAdapter): this;
11
+ withAuth(auth: IAuthAdapter): this;
12
+ withStorage(storage: IStorageAdapter): this;
13
+ withReconnection(config: NonNullable<ClientConfig['reconnection']>): this;
14
+ build(): BaseClient;
15
+ }
16
+ //# sourceMappingURL=ClientBuilder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ClientBuilder.d.ts","sourceRoot":"","sources":["../../src/client/ClientBuilder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AAC1E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAE7D,qBAAa,aAAa;IACxB,OAAO,CAAC,SAAS,CAAC,CAAoB;IACtC,OAAO,CAAC,IAAI,CAAC,CAAe;IAC5B,OAAO,CAAC,OAAO,CAAC,CAAkB;IAClC,OAAO,CAAC,YAAY,CAAC,CAA+B;IAEpD,aAAa,CAAC,SAAS,EAAE,iBAAiB,GAAG,IAAI;IAKjD,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IAKlC,WAAW,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI;IAK3C,gBAAgB,CAAC,MAAM,EAAE,WAAW,CAAC,YAAY,CAAC,cAAc,CAAC,CAAC,GAAG,IAAI;IAKzE,KAAK,IAAI,UAAU;CAoBpB"}
@@ -0,0 +1,37 @@
1
+ import { BaseClient } from './BaseClient';
2
+ export class ClientBuilder {
3
+ withTransport(transport) {
4
+ this.transport = transport;
5
+ return this;
6
+ }
7
+ withAuth(auth) {
8
+ this.auth = auth;
9
+ return this;
10
+ }
11
+ withStorage(storage) {
12
+ this.storage = storage;
13
+ return this;
14
+ }
15
+ withReconnection(config) {
16
+ this.reconnection = config;
17
+ return this;
18
+ }
19
+ build() {
20
+ if (!this.transport) {
21
+ throw new Error('Transport adapter is required');
22
+ }
23
+ const config = {
24
+ transport: this.transport,
25
+ auth: this.auth,
26
+ storage: this.storage,
27
+ reconnection: this.reconnection || {
28
+ enabled: true,
29
+ maxAttempts: 5,
30
+ initialDelay: 1000,
31
+ maxDelay: 30000,
32
+ backoffFactor: 2
33
+ }
34
+ };
35
+ return new BaseClient(config);
36
+ }
37
+ }
@@ -0,0 +1,3 @@
1
+ export { BaseClient, type ClientConfig, type ClientEvents } from './BaseClient';
2
+ export { ClientBuilder } from './ClientBuilder';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAChF,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,3 @@
1
+ // Client-side implementations
2
+ export { BaseClient } from './BaseClient';
3
+ export { ClientBuilder } from './ClientBuilder';
@@ -0,0 +1,7 @@
1
+ export { EventType, BaseEvent, FileAction, FileChangeEvent, GitStatusEvent, CommitEvent, BranchChangeEvent, CursorPositionEvent, Event, TokenPayload, AuthResult, CredentialType, OAuthProvider, Credentials, JWTConfig, OAuthConfig, AuthProvider, RoomPermission, Room, RoomUser, UserStatus, RoomState, RoomConfig, LockType, LockPriority, Lock, LockRequest, LockQueueItem, LockState, ConnectionState, ConnectionOptions, Message, MessageHandler, ErrorHandler, CloseHandler } from './types';
2
+ export { ITransportAdapter, IStorageAdapter, StorageOperation, IAuthAdapter, TypedEventEmitter, EventListener, UnsubscribeFn, RoomManager, LockManager, DefaultRoomManager, DefaultLockManager } from './abstractions';
3
+ export { MockTransportAdapter, MockStorageAdapter, MockAuthAdapter } from './adapters/mock';
4
+ export { WebSocketTransportAdapter } from './adapters/websocket';
5
+ export { BaseClient, ClientBuilder, type ClientConfig, type ClientEvents } from './client';
6
+ export { BaseServer, ServerBuilder, type ServerConfig, type ServerEvents, type ConnectedClient } from './server';
7
+ //# sourceMappingURL=index.d.ts.map