@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.
- package/README.md +84 -0
- package/dist/abstractions/AuthAdapter.d.ts +9 -0
- package/dist/abstractions/AuthAdapter.d.ts.map +1 -0
- package/dist/abstractions/AuthAdapter.js +1 -0
- package/dist/abstractions/DefaultLockManager.d.ts +18 -0
- package/dist/abstractions/DefaultLockManager.d.ts.map +1 -0
- package/dist/abstractions/DefaultLockManager.js +152 -0
- package/dist/abstractions/DefaultRoomManager.d.ts +15 -0
- package/dist/abstractions/DefaultRoomManager.d.ts.map +1 -0
- package/dist/abstractions/DefaultRoomManager.js +91 -0
- package/dist/abstractions/EventEmitter.d.ts +14 -0
- package/dist/abstractions/EventEmitter.d.ts.map +1 -0
- package/dist/abstractions/EventEmitter.js +56 -0
- package/dist/abstractions/LockManager.d.ts +16 -0
- package/dist/abstractions/LockManager.d.ts.map +1 -0
- package/dist/abstractions/LockManager.js +9 -0
- package/dist/abstractions/RoomManager.d.ts +15 -0
- package/dist/abstractions/RoomManager.d.ts.map +1 -0
- package/dist/abstractions/RoomManager.js +5 -0
- package/dist/abstractions/StorageAdapter.d.ts +25 -0
- package/dist/abstractions/StorageAdapter.d.ts.map +1 -0
- package/dist/abstractions/StorageAdapter.js +1 -0
- package/dist/abstractions/TransportAdapter.d.ts +17 -0
- package/dist/abstractions/TransportAdapter.d.ts.map +1 -0
- package/dist/abstractions/TransportAdapter.js +1 -0
- package/dist/abstractions/index.d.ts +9 -0
- package/dist/abstractions/index.d.ts.map +1 -0
- package/dist/abstractions/index.js +5 -0
- package/dist/adapters/mock/MockAuthAdapter.d.ts +27 -0
- package/dist/adapters/mock/MockAuthAdapter.d.ts.map +1 -0
- package/dist/adapters/mock/MockAuthAdapter.js +161 -0
- package/dist/adapters/mock/MockStorageAdapter.d.ts +27 -0
- package/dist/adapters/mock/MockStorageAdapter.d.ts.map +1 -0
- package/dist/adapters/mock/MockStorageAdapter.js +162 -0
- package/dist/adapters/mock/MockTransportAdapter.d.ts +31 -0
- package/dist/adapters/mock/MockTransportAdapter.d.ts.map +1 -0
- package/dist/adapters/mock/MockTransportAdapter.js +90 -0
- package/dist/adapters/mock/index.d.ts +4 -0
- package/dist/adapters/mock/index.d.ts.map +1 -0
- package/dist/adapters/mock/index.js +3 -0
- package/dist/adapters/websocket/WebSocketTransportAdapter.d.ts +40 -0
- package/dist/adapters/websocket/WebSocketTransportAdapter.d.ts.map +1 -0
- package/dist/adapters/websocket/WebSocketTransportAdapter.js +204 -0
- package/dist/adapters/websocket/index.d.ts +2 -0
- package/dist/adapters/websocket/index.d.ts.map +1 -0
- package/dist/adapters/websocket/index.js +1 -0
- package/dist/client/BaseClient.d.ts +103 -0
- package/dist/client/BaseClient.d.ts.map +1 -0
- package/dist/client/BaseClient.js +282 -0
- package/dist/client/ClientBuilder.d.ts +16 -0
- package/dist/client/ClientBuilder.d.ts.map +1 -0
- package/dist/client/ClientBuilder.js +37 -0
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +3 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -1205
- package/dist/index.js.map +23 -6
- package/dist/server/BaseServer.d.ts +116 -0
- package/dist/server/BaseServer.d.ts.map +1 -0
- package/dist/server/BaseServer.js +422 -0
- package/dist/server/ServerBuilder.d.ts +32 -0
- package/dist/server/ServerBuilder.d.ts.map +1 -0
- package/dist/server/ServerBuilder.js +66 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +3 -0
- package/dist/types/auth.d.ts +40 -0
- package/dist/types/auth.d.ts.map +1 -0
- package/dist/types/auth.js +1 -0
- package/dist/types/events.d.ts +66 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/events.js +1 -0
- package/dist/types/index.d.ts +23 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/lock.d.ts +35 -0
- package/dist/types/lock.d.ts.map +1 -0
- package/dist/types/lock.js +1 -0
- package/dist/types/room.d.ts +40 -0
- package/dist/types/room.d.ts.map +1 -0
- package/dist/types/room.js +1 -0
- 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 @@
|
|
|
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 @@
|
|
|
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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|