@principal-ai/control-tower-core 0.1.7 → 0.1.8
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 +53 -0
- package/dist/abstractions/DefaultPresenceManager.d.ts +40 -0
- package/dist/abstractions/DefaultPresenceManager.d.ts.map +1 -0
- package/dist/abstractions/DefaultPresenceManager.js +256 -0
- package/dist/abstractions/PresenceManager.d.ts +127 -0
- package/dist/abstractions/PresenceManager.d.ts.map +1 -0
- package/dist/abstractions/PresenceManager.js +80 -0
- package/dist/abstractions/index.d.ts +2 -0
- package/dist/abstractions/index.d.ts.map +1 -1
- package/dist/abstractions/index.js +5 -1
- package/dist/adapters/mock/MockTransportAdapter.d.ts +37 -1
- package/dist/adapters/mock/MockTransportAdapter.d.ts.map +1 -1
- package/dist/adapters/mock/MockTransportAdapter.js +101 -2
- package/dist/adapters/websocket/WebSocketTransportAdapter.d.ts.map +1 -1
- package/dist/adapters/websocket/WebSocketTransportAdapter.js +5 -3
- package/dist/index.js.map +9 -7
- package/dist/index.mjs +511 -30
- package/dist/index.mjs.map +9 -7
- package/dist/server/BaseServer.d.ts +59 -0
- package/dist/server/BaseServer.d.ts.map +1 -1
- package/dist/server/BaseServer.js +219 -28
- package/dist/server/ServerBuilder.d.ts +11 -0
- package/dist/server/ServerBuilder.d.ts.map +1 -1
- package/dist/server/ServerBuilder.js +13 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/presence.d.ts +163 -0
- package/dist/types/presence.d.ts.map +1 -0
- package/dist/types/presence.js +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -136,11 +136,64 @@ await server.experimental.sendToUsers(
|
|
|
136
136
|
- Use for prototyping, then submit a [feature request](docs/FEATURE_REQUEST_TEMPLATE.md) to graduate to stable API
|
|
137
137
|
- See [full documentation](docs/EXPERIMENTAL_BROADCAST.md) for details
|
|
138
138
|
|
|
139
|
+
## Testing
|
|
140
|
+
|
|
141
|
+
Control Tower Core provides comprehensive test utilities for building reliable applications.
|
|
142
|
+
|
|
143
|
+
### MockTransportAdapter
|
|
144
|
+
|
|
145
|
+
The `MockTransportAdapter` provides test helpers for simulating client-server interactions:
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
import { MockTransportAdapter } from '@principal-ai/control-tower-core';
|
|
149
|
+
|
|
150
|
+
const transport = new MockTransportAdapter();
|
|
151
|
+
|
|
152
|
+
// Simulate a client connection
|
|
153
|
+
await transport.simulateConnection('client-id', {
|
|
154
|
+
authenticated: true,
|
|
155
|
+
userId: 'user-123',
|
|
156
|
+
metadata: { deviceType: 'desktop' }
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Simulate client messages
|
|
160
|
+
await transport.simulateClientMessage('client-id', 'join_room', {
|
|
161
|
+
roomId: 'room-1'
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Simulate incoming server messages (for client tests)
|
|
165
|
+
transport.simulateIncomingMessage({
|
|
166
|
+
type: 'room_joined',
|
|
167
|
+
payload: { roomId: 'room-1', state: { ... } }
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Track connection attempts (useful for reconnection tests)
|
|
171
|
+
const attempts = transport.getConnectionAttempts();
|
|
172
|
+
transport.resetConnectionAttempts();
|
|
173
|
+
|
|
174
|
+
// Get messages sent to specific clients
|
|
175
|
+
const messages = transport.getSentMessages('client-id');
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Running Tests
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
# Run all tests
|
|
182
|
+
bun test
|
|
183
|
+
|
|
184
|
+
# Run specific test file
|
|
185
|
+
bun test tests/BaseServer.test.ts
|
|
186
|
+
|
|
187
|
+
# Run with coverage
|
|
188
|
+
bun test --coverage
|
|
189
|
+
```
|
|
190
|
+
|
|
139
191
|
## Documentation
|
|
140
192
|
|
|
141
193
|
- [Experimental Broadcast APIs](docs/EXPERIMENTAL_BROADCAST.md)
|
|
142
194
|
- [Feature Request Template](docs/FEATURE_REQUEST_TEMPLATE.md)
|
|
143
195
|
- [Architecture Design](DESIGN.md)
|
|
196
|
+
- [Changelog](CHANGELOG.md)
|
|
144
197
|
|
|
145
198
|
## License
|
|
146
199
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default In-Memory Presence Manager
|
|
3
|
+
*
|
|
4
|
+
* Reference implementation of PresenceManager using in-memory storage.
|
|
5
|
+
* Suitable for single-server deployments and development.
|
|
6
|
+
*
|
|
7
|
+
* For multi-server deployments, implement a custom PresenceManager
|
|
8
|
+
* backed by Redis or another shared data store.
|
|
9
|
+
*/
|
|
10
|
+
import { PresenceManager } from './PresenceManager.js';
|
|
11
|
+
import type { UserPresence, DeviceInfo, PresenceStatus, PresenceConfig, PresenceChangeEvent, ActivityUpdate } from '../types/presence.js';
|
|
12
|
+
export declare class DefaultPresenceManager extends PresenceManager {
|
|
13
|
+
private userPresences;
|
|
14
|
+
private deviceToUser;
|
|
15
|
+
private gracePeriodEntries;
|
|
16
|
+
constructor(config?: PresenceConfig);
|
|
17
|
+
connectDevice(userId: string, deviceId: string, deviceInfo?: Partial<DeviceInfo>): Promise<UserPresence>;
|
|
18
|
+
disconnectDevice(userId: string, deviceId: string): Promise<UserPresence | null>;
|
|
19
|
+
updateActivity(update: ActivityUpdate): Promise<UserPresence>;
|
|
20
|
+
getUserPresence(userId: string): Promise<UserPresence | null>;
|
|
21
|
+
getUsersPresence(userIds: string[]): Promise<Map<string, UserPresence>>;
|
|
22
|
+
getOnlineUsers(): Promise<UserPresence[]>;
|
|
23
|
+
getUserDevices(userId: string): Promise<DeviceInfo[]>;
|
|
24
|
+
setUserStatus(userId: string, status: PresenceStatus): Promise<UserPresence>;
|
|
25
|
+
processHeartbeats(): Promise<PresenceChangeEvent[]>;
|
|
26
|
+
clear(): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Get count of users in grace period (useful for monitoring)
|
|
29
|
+
*/
|
|
30
|
+
getGracePeriodCount(): number;
|
|
31
|
+
/**
|
|
32
|
+
* Get total connected device count
|
|
33
|
+
*/
|
|
34
|
+
getConnectedDeviceCount(): number;
|
|
35
|
+
/**
|
|
36
|
+
* Get total unique user count
|
|
37
|
+
*/
|
|
38
|
+
getUniqueUserCount(): number;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=DefaultPresenceManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DefaultPresenceManager.d.ts","sourceRoot":"","sources":["../../src/abstractions/DefaultPresenceManager.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,KAAK,EACV,YAAY,EACZ,UAAU,EACV,cAAc,EACd,cAAc,EACd,mBAAmB,EACnB,cAAc,EACf,MAAM,sBAAsB,CAAC;AAW9B,qBAAa,sBAAuB,SAAQ,eAAe;IACzD,OAAO,CAAC,aAAa,CAAmC;IACxD,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,kBAAkB,CAAuC;gBAErD,MAAM,CAAC,EAAE,cAAc;IAI7B,aAAa,CACjB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,GAC/B,OAAO,CAAC,YAAY,CAAC;IA+DlB,gBAAgB,CACpB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IA6CzB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC;IA4B7D,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAI7D,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAavE,cAAc,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAYzC,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IASrD,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC;IAU5E,iBAAiB,IAAI,OAAO,CAAC,mBAAmB,EAAE,CAAC;IA4EnD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAM5B;;OAEG;IACH,mBAAmB,IAAI,MAAM;IAI7B;;OAEG;IACH,uBAAuB,IAAI,MAAM;IAIjC;;OAEG;IACH,kBAAkB,IAAI,MAAM;CAG7B"}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Default In-Memory Presence Manager
|
|
4
|
+
*
|
|
5
|
+
* Reference implementation of PresenceManager using in-memory storage.
|
|
6
|
+
* Suitable for single-server deployments and development.
|
|
7
|
+
*
|
|
8
|
+
* For multi-server deployments, implement a custom PresenceManager
|
|
9
|
+
* backed by Redis or another shared data store.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.DefaultPresenceManager = void 0;
|
|
13
|
+
const PresenceManager_js_1 = require("./PresenceManager.js");
|
|
14
|
+
class DefaultPresenceManager extends PresenceManager_js_1.PresenceManager {
|
|
15
|
+
constructor(config) {
|
|
16
|
+
super(config);
|
|
17
|
+
this.userPresences = new Map();
|
|
18
|
+
this.deviceToUser = new Map();
|
|
19
|
+
this.gracePeriodEntries = new Map();
|
|
20
|
+
}
|
|
21
|
+
async connectDevice(userId, deviceId, deviceInfo) {
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
// Check if user is in grace period (reconnecting)
|
|
24
|
+
const gracePeriodEntry = this.gracePeriodEntries.get(userId);
|
|
25
|
+
if (gracePeriodEntry) {
|
|
26
|
+
// Restore from grace period
|
|
27
|
+
this.gracePeriodEntries.delete(userId);
|
|
28
|
+
const presence = gracePeriodEntry.previousPresence;
|
|
29
|
+
// Add the new device
|
|
30
|
+
const device = {
|
|
31
|
+
deviceId,
|
|
32
|
+
type: deviceInfo?.type,
|
|
33
|
+
connectedAt: now,
|
|
34
|
+
lastActivity: now,
|
|
35
|
+
metadata: deviceInfo?.metadata
|
|
36
|
+
};
|
|
37
|
+
presence.devices.set(deviceId, device);
|
|
38
|
+
presence.status = 'online';
|
|
39
|
+
presence.lastActivity = now;
|
|
40
|
+
this.userPresences.set(userId, presence);
|
|
41
|
+
this.deviceToUser.set(deviceId, userId);
|
|
42
|
+
return presence;
|
|
43
|
+
}
|
|
44
|
+
// Get or create user presence
|
|
45
|
+
let presence = this.userPresences.get(userId);
|
|
46
|
+
if (!presence) {
|
|
47
|
+
// New user
|
|
48
|
+
presence = {
|
|
49
|
+
userId,
|
|
50
|
+
status: 'online',
|
|
51
|
+
devices: new Map(),
|
|
52
|
+
firstConnectedAt: now,
|
|
53
|
+
lastActivity: now,
|
|
54
|
+
metadata: {}
|
|
55
|
+
};
|
|
56
|
+
this.userPresences.set(userId, presence);
|
|
57
|
+
}
|
|
58
|
+
// Add device
|
|
59
|
+
const device = {
|
|
60
|
+
deviceId,
|
|
61
|
+
type: deviceInfo?.type,
|
|
62
|
+
connectedAt: now,
|
|
63
|
+
lastActivity: now,
|
|
64
|
+
metadata: deviceInfo?.metadata
|
|
65
|
+
};
|
|
66
|
+
presence.devices.set(deviceId, device);
|
|
67
|
+
presence.status = this.calculatePresenceStatus(presence.devices, now);
|
|
68
|
+
presence.lastActivity = now;
|
|
69
|
+
this.deviceToUser.set(deviceId, userId);
|
|
70
|
+
return presence;
|
|
71
|
+
}
|
|
72
|
+
async disconnectDevice(userId, deviceId) {
|
|
73
|
+
const presence = this.userPresences.get(userId);
|
|
74
|
+
if (!presence) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
// Remove device
|
|
78
|
+
presence.devices.delete(deviceId);
|
|
79
|
+
this.deviceToUser.delete(deviceId);
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
// If no devices left, start grace period
|
|
82
|
+
if (presence.devices.size === 0) {
|
|
83
|
+
if (this.config.gracePeriod > 0) {
|
|
84
|
+
// Move to grace period
|
|
85
|
+
this.gracePeriodEntries.set(userId, {
|
|
86
|
+
userId,
|
|
87
|
+
disconnectedAt: now,
|
|
88
|
+
previousPresence: presence
|
|
89
|
+
});
|
|
90
|
+
this.userPresences.delete(userId);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
// No grace period, remove completely
|
|
95
|
+
this.userPresences.delete(userId);
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Still has devices, update status
|
|
100
|
+
presence.status = this.calculatePresenceStatus(presence.devices, now);
|
|
101
|
+
// Update lastActivity to most recent device activity
|
|
102
|
+
let mostRecentActivity = 0;
|
|
103
|
+
for (const device of presence.devices.values()) {
|
|
104
|
+
if (device.lastActivity > mostRecentActivity) {
|
|
105
|
+
mostRecentActivity = device.lastActivity;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
presence.lastActivity = mostRecentActivity;
|
|
109
|
+
return presence;
|
|
110
|
+
}
|
|
111
|
+
async updateActivity(update) {
|
|
112
|
+
const { userId, deviceId, timestamp } = update;
|
|
113
|
+
const presence = this.userPresences.get(userId);
|
|
114
|
+
if (!presence) {
|
|
115
|
+
throw new Error(`User ${userId} not found in presence system`);
|
|
116
|
+
}
|
|
117
|
+
const device = presence.devices.get(deviceId);
|
|
118
|
+
if (!device) {
|
|
119
|
+
throw new Error(`Device ${deviceId} not found for user ${userId}`);
|
|
120
|
+
}
|
|
121
|
+
// Update device activity
|
|
122
|
+
device.lastActivity = timestamp;
|
|
123
|
+
// Update user activity
|
|
124
|
+
if (timestamp > presence.lastActivity) {
|
|
125
|
+
presence.lastActivity = timestamp;
|
|
126
|
+
}
|
|
127
|
+
// Recalculate status
|
|
128
|
+
const previousStatus = presence.status;
|
|
129
|
+
presence.status = this.calculatePresenceStatus(presence.devices, timestamp);
|
|
130
|
+
return presence;
|
|
131
|
+
}
|
|
132
|
+
async getUserPresence(userId) {
|
|
133
|
+
return this.userPresences.get(userId) || null;
|
|
134
|
+
}
|
|
135
|
+
async getUsersPresence(userIds) {
|
|
136
|
+
const result = new Map();
|
|
137
|
+
for (const userId of userIds) {
|
|
138
|
+
const presence = this.userPresences.get(userId);
|
|
139
|
+
if (presence) {
|
|
140
|
+
result.set(userId, presence);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
async getOnlineUsers() {
|
|
146
|
+
const online = [];
|
|
147
|
+
for (const presence of this.userPresences.values()) {
|
|
148
|
+
if (presence.status === 'online') {
|
|
149
|
+
online.push(presence);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return online;
|
|
153
|
+
}
|
|
154
|
+
async getUserDevices(userId) {
|
|
155
|
+
const presence = this.userPresences.get(userId);
|
|
156
|
+
if (!presence) {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
return Array.from(presence.devices.values());
|
|
160
|
+
}
|
|
161
|
+
async setUserStatus(userId, status) {
|
|
162
|
+
const presence = this.userPresences.get(userId);
|
|
163
|
+
if (!presence) {
|
|
164
|
+
throw new Error(`User ${userId} not found in presence system`);
|
|
165
|
+
}
|
|
166
|
+
presence.status = status;
|
|
167
|
+
return presence;
|
|
168
|
+
}
|
|
169
|
+
async processHeartbeats() {
|
|
170
|
+
const now = Date.now();
|
|
171
|
+
const changes = [];
|
|
172
|
+
// Process active presences
|
|
173
|
+
for (const [userId, presence] of this.userPresences.entries()) {
|
|
174
|
+
const devicesToRemove = [];
|
|
175
|
+
// Check each device for timeout
|
|
176
|
+
for (const [deviceId, device] of presence.devices.entries()) {
|
|
177
|
+
if (this.shouldDisconnectDevice(device, now)) {
|
|
178
|
+
devicesToRemove.push(deviceId);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Remove timed-out devices
|
|
182
|
+
for (const deviceId of devicesToRemove) {
|
|
183
|
+
presence.devices.delete(deviceId);
|
|
184
|
+
this.deviceToUser.delete(deviceId);
|
|
185
|
+
}
|
|
186
|
+
// Update status after removing devices
|
|
187
|
+
const previousStatus = presence.status;
|
|
188
|
+
const newStatus = this.calculatePresenceStatus(presence.devices, now);
|
|
189
|
+
if (newStatus !== previousStatus) {
|
|
190
|
+
presence.status = newStatus;
|
|
191
|
+
changes.push({
|
|
192
|
+
userId,
|
|
193
|
+
previousStatus,
|
|
194
|
+
status: newStatus,
|
|
195
|
+
timestamp: now,
|
|
196
|
+
reason: 'heartbeat_timeout'
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
// If no devices left, move to grace period
|
|
200
|
+
if (presence.devices.size === 0) {
|
|
201
|
+
if (this.config.gracePeriod > 0) {
|
|
202
|
+
this.gracePeriodEntries.set(userId, {
|
|
203
|
+
userId,
|
|
204
|
+
disconnectedAt: now,
|
|
205
|
+
previousPresence: presence
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
this.userPresences.delete(userId);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// Process grace period expirations
|
|
212
|
+
const expiredGracePeriods = [];
|
|
213
|
+
for (const [userId, entry] of this.gracePeriodEntries.entries()) {
|
|
214
|
+
const gracePeriodElapsed = now - entry.disconnectedAt;
|
|
215
|
+
if (gracePeriodElapsed > this.config.gracePeriod) {
|
|
216
|
+
expiredGracePeriods.push(userId);
|
|
217
|
+
changes.push({
|
|
218
|
+
userId,
|
|
219
|
+
previousStatus: entry.previousPresence.status,
|
|
220
|
+
status: 'offline',
|
|
221
|
+
timestamp: now,
|
|
222
|
+
reason: 'grace_period_expired'
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// Remove expired grace period entries
|
|
227
|
+
for (const userId of expiredGracePeriods) {
|
|
228
|
+
this.gracePeriodEntries.delete(userId);
|
|
229
|
+
}
|
|
230
|
+
return changes;
|
|
231
|
+
}
|
|
232
|
+
async clear() {
|
|
233
|
+
this.userPresences.clear();
|
|
234
|
+
this.deviceToUser.clear();
|
|
235
|
+
this.gracePeriodEntries.clear();
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Get count of users in grace period (useful for monitoring)
|
|
239
|
+
*/
|
|
240
|
+
getGracePeriodCount() {
|
|
241
|
+
return this.gracePeriodEntries.size;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Get total connected device count
|
|
245
|
+
*/
|
|
246
|
+
getConnectedDeviceCount() {
|
|
247
|
+
return this.deviceToUser.size;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Get total unique user count
|
|
251
|
+
*/
|
|
252
|
+
getUniqueUserCount() {
|
|
253
|
+
return this.userPresences.size + this.gracePeriodEntries.size;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
exports.DefaultPresenceManager = DefaultPresenceManager;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presence Manager Abstract Class
|
|
3
|
+
*
|
|
4
|
+
* Manages global user presence tracking independent of room membership.
|
|
5
|
+
* Supports multi-device connections, activity monitoring, and grace periods.
|
|
6
|
+
*/
|
|
7
|
+
import type { UserPresence, DeviceInfo, PresenceStatus, PresenceConfig, PresenceChangeEvent, ActivityUpdate } from '../types/presence.js';
|
|
8
|
+
/**
|
|
9
|
+
* Abstract class for managing user presence
|
|
10
|
+
*
|
|
11
|
+
* Implementations should handle:
|
|
12
|
+
* - Multi-device tracking per user
|
|
13
|
+
* - Activity/heartbeat monitoring
|
|
14
|
+
* - Grace period for disconnects
|
|
15
|
+
* - Presence state persistence (if needed)
|
|
16
|
+
*/
|
|
17
|
+
export declare abstract class PresenceManager {
|
|
18
|
+
protected config: Required<PresenceConfig>;
|
|
19
|
+
constructor(config?: PresenceConfig);
|
|
20
|
+
/**
|
|
21
|
+
* Register a new device connection for a user
|
|
22
|
+
*
|
|
23
|
+
* @param userId - User identifier
|
|
24
|
+
* @param deviceId - Device/client identifier
|
|
25
|
+
* @param deviceInfo - Optional device metadata
|
|
26
|
+
* @returns The updated user presence
|
|
27
|
+
*/
|
|
28
|
+
abstract connectDevice(userId: string, deviceId: string, deviceInfo?: Partial<DeviceInfo>): Promise<UserPresence>;
|
|
29
|
+
/**
|
|
30
|
+
* Unregister a device connection
|
|
31
|
+
*
|
|
32
|
+
* May trigger grace period if this is the user's last device.
|
|
33
|
+
*
|
|
34
|
+
* @param userId - User identifier
|
|
35
|
+
* @param deviceId - Device/client identifier
|
|
36
|
+
* @returns The updated user presence, or null if user is completely offline
|
|
37
|
+
*/
|
|
38
|
+
abstract disconnectDevice(userId: string, deviceId: string): Promise<UserPresence | null>;
|
|
39
|
+
/**
|
|
40
|
+
* Update activity timestamp for a device
|
|
41
|
+
*
|
|
42
|
+
* Used for heartbeats and activity tracking.
|
|
43
|
+
*
|
|
44
|
+
* @param update - Activity update information
|
|
45
|
+
* @returns The updated user presence
|
|
46
|
+
*/
|
|
47
|
+
abstract updateActivity(update: ActivityUpdate): Promise<UserPresence>;
|
|
48
|
+
/**
|
|
49
|
+
* Get presence state for a user
|
|
50
|
+
*
|
|
51
|
+
* @param userId - User identifier
|
|
52
|
+
* @returns User presence, or null if not found
|
|
53
|
+
*/
|
|
54
|
+
abstract getUserPresence(userId: string): Promise<UserPresence | null>;
|
|
55
|
+
/**
|
|
56
|
+
* Get presence state for multiple users
|
|
57
|
+
*
|
|
58
|
+
* @param userIds - Array of user identifiers
|
|
59
|
+
* @returns Map of userId to UserPresence
|
|
60
|
+
*/
|
|
61
|
+
abstract getUsersPresence(userIds: string[]): Promise<Map<string, UserPresence>>;
|
|
62
|
+
/**
|
|
63
|
+
* Get all online users
|
|
64
|
+
*
|
|
65
|
+
* @returns Array of user presences with status 'online'
|
|
66
|
+
*/
|
|
67
|
+
abstract getOnlineUsers(): Promise<UserPresence[]>;
|
|
68
|
+
/**
|
|
69
|
+
* Get all devices for a user
|
|
70
|
+
*
|
|
71
|
+
* @param userId - User identifier
|
|
72
|
+
* @returns Array of device info
|
|
73
|
+
*/
|
|
74
|
+
abstract getUserDevices(userId: string): Promise<DeviceInfo[]>;
|
|
75
|
+
/**
|
|
76
|
+
* Manually set user status
|
|
77
|
+
*
|
|
78
|
+
* @param userId - User identifier
|
|
79
|
+
* @param status - New presence status
|
|
80
|
+
* @returns The updated user presence
|
|
81
|
+
*/
|
|
82
|
+
abstract setUserStatus(userId: string, status: PresenceStatus): Promise<UserPresence>;
|
|
83
|
+
/**
|
|
84
|
+
* Check for stale connections and update presence accordingly
|
|
85
|
+
*
|
|
86
|
+
* This should be called periodically to:
|
|
87
|
+
* - Mark idle devices as 'away'
|
|
88
|
+
* - Disconnect devices that haven't sent heartbeats
|
|
89
|
+
* - Expire grace periods
|
|
90
|
+
*
|
|
91
|
+
* @returns Array of presence change events
|
|
92
|
+
*/
|
|
93
|
+
abstract processHeartbeats(): Promise<PresenceChangeEvent[]>;
|
|
94
|
+
/**
|
|
95
|
+
* Clear all presence data
|
|
96
|
+
*
|
|
97
|
+
* Useful for testing and cleanup.
|
|
98
|
+
*/
|
|
99
|
+
abstract clear(): Promise<void>;
|
|
100
|
+
/**
|
|
101
|
+
* Get configuration
|
|
102
|
+
*/
|
|
103
|
+
getConfig(): Readonly<Required<PresenceConfig>>;
|
|
104
|
+
/**
|
|
105
|
+
* Check if presence tracking is enabled
|
|
106
|
+
*/
|
|
107
|
+
isEnabled(): boolean;
|
|
108
|
+
/**
|
|
109
|
+
* Calculate presence status based on device states
|
|
110
|
+
*
|
|
111
|
+
* Helper method for implementations to determine overall user status.
|
|
112
|
+
*
|
|
113
|
+
* @param devices - Map of devices for a user
|
|
114
|
+
* @param now - Current timestamp
|
|
115
|
+
* @returns Calculated presence status
|
|
116
|
+
*/
|
|
117
|
+
protected calculatePresenceStatus(devices: Map<string, DeviceInfo>, now?: number): PresenceStatus;
|
|
118
|
+
/**
|
|
119
|
+
* Check if a device should be disconnected due to missed heartbeats
|
|
120
|
+
*
|
|
121
|
+
* @param device - Device info
|
|
122
|
+
* @param now - Current timestamp
|
|
123
|
+
* @returns true if device should be disconnected
|
|
124
|
+
*/
|
|
125
|
+
protected shouldDisconnectDevice(device: DeviceInfo, now?: number): boolean;
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=PresenceManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PresenceManager.d.ts","sourceRoot":"","sources":["../../src/abstractions/PresenceManager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,UAAU,EACV,cAAc,EACd,cAAc,EACd,mBAAmB,EACnB,cAAc,EACf,MAAM,sBAAsB,CAAC;AAE9B;;;;;;;;GAQG;AACH,8BAAsB,eAAe;IACnC,SAAS,CAAC,MAAM,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC;gBAE/B,MAAM,CAAC,EAAE,cAAc;IAanC;;;;;;;OAOG;IACH,QAAQ,CAAC,aAAa,CACpB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,GAC/B,OAAO,CAAC,YAAY,CAAC;IAExB;;;;;;;;OAQG;IACH,QAAQ,CAAC,gBAAgB,CACvB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAE/B;;;;;;;OAOG;IACH,QAAQ,CAAC,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC;IAEtE;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAEtE;;;;;OAKG;IACH,QAAQ,CAAC,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAEhF;;;;OAIG;IACH,QAAQ,CAAC,cAAc,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAElD;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAE9D;;;;;;OAMG;IACH,QAAQ,CAAC,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC;IAErF;;;;;;;;;OASG;IACH,QAAQ,CAAC,iBAAiB,IAAI,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAE5D;;;;OAIG;IACH,QAAQ,CAAC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAE/B;;OAEG;IACH,SAAS,IAAI,QAAQ,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC;IAI/C;;OAEG;IACH,SAAS,IAAI,OAAO;IAIpB;;;;;;;;OAQG;IACH,SAAS,CAAC,uBAAuB,CAC/B,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,EAChC,GAAG,GAAE,MAAmB,GACvB,cAAc;IAmBjB;;;;;;OAMG;IACH,SAAS,CAAC,sBAAsB,CAC9B,MAAM,EAAE,UAAU,EAClB,GAAG,GAAE,MAAmB,GACvB,OAAO;CAIX"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Presence Manager Abstract Class
|
|
4
|
+
*
|
|
5
|
+
* Manages global user presence tracking independent of room membership.
|
|
6
|
+
* Supports multi-device connections, activity monitoring, and grace periods.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.PresenceManager = void 0;
|
|
10
|
+
/**
|
|
11
|
+
* Abstract class for managing user presence
|
|
12
|
+
*
|
|
13
|
+
* Implementations should handle:
|
|
14
|
+
* - Multi-device tracking per user
|
|
15
|
+
* - Activity/heartbeat monitoring
|
|
16
|
+
* - Grace period for disconnects
|
|
17
|
+
* - Presence state persistence (if needed)
|
|
18
|
+
*/
|
|
19
|
+
class PresenceManager {
|
|
20
|
+
constructor(config) {
|
|
21
|
+
// Set defaults
|
|
22
|
+
this.config = {
|
|
23
|
+
enabled: config?.enabled ?? false,
|
|
24
|
+
heartbeatInterval: config?.heartbeatInterval ?? 30000,
|
|
25
|
+
awayThreshold: config?.awayThreshold ?? 2,
|
|
26
|
+
disconnectThreshold: config?.disconnectThreshold ?? 3,
|
|
27
|
+
gracePeriod: config?.gracePeriod ?? 30000,
|
|
28
|
+
trackDeviceActivity: config?.trackDeviceActivity ?? true,
|
|
29
|
+
broadcastPresenceUpdates: config?.broadcastPresenceUpdates ?? true
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Get configuration
|
|
34
|
+
*/
|
|
35
|
+
getConfig() {
|
|
36
|
+
return { ...this.config };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Check if presence tracking is enabled
|
|
40
|
+
*/
|
|
41
|
+
isEnabled() {
|
|
42
|
+
return this.config.enabled;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Calculate presence status based on device states
|
|
46
|
+
*
|
|
47
|
+
* Helper method for implementations to determine overall user status.
|
|
48
|
+
*
|
|
49
|
+
* @param devices - Map of devices for a user
|
|
50
|
+
* @param now - Current timestamp
|
|
51
|
+
* @returns Calculated presence status
|
|
52
|
+
*/
|
|
53
|
+
calculatePresenceStatus(devices, now = Date.now()) {
|
|
54
|
+
if (devices.size === 0) {
|
|
55
|
+
return 'offline';
|
|
56
|
+
}
|
|
57
|
+
const heartbeatTimeout = this.config.heartbeatInterval * this.config.awayThreshold;
|
|
58
|
+
// Check if ANY device is active (within heartbeat threshold)
|
|
59
|
+
for (const device of devices.values()) {
|
|
60
|
+
const timeSinceActivity = now - device.lastActivity;
|
|
61
|
+
if (timeSinceActivity < heartbeatTimeout) {
|
|
62
|
+
return 'online';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// All devices are idle
|
|
66
|
+
return 'away';
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Check if a device should be disconnected due to missed heartbeats
|
|
70
|
+
*
|
|
71
|
+
* @param device - Device info
|
|
72
|
+
* @param now - Current timestamp
|
|
73
|
+
* @returns true if device should be disconnected
|
|
74
|
+
*/
|
|
75
|
+
shouldDisconnectDevice(device, now = Date.now()) {
|
|
76
|
+
const timeout = this.config.heartbeatInterval * this.config.disconnectThreshold;
|
|
77
|
+
return (now - device.lastActivity) > timeout;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
exports.PresenceManager = PresenceManager;
|
|
@@ -6,4 +6,6 @@ export { RoomManager } from './RoomManager.js';
|
|
|
6
6
|
export { LockManager } from './LockManager.js';
|
|
7
7
|
export { DefaultRoomManager } from './DefaultRoomManager.js';
|
|
8
8
|
export { DefaultLockManager } from './DefaultLockManager.js';
|
|
9
|
+
export { PresenceManager } from './PresenceManager.js';
|
|
10
|
+
export { DefaultPresenceManager } from './DefaultPresenceManager.js';
|
|
9
11
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/abstractions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACxE,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACpF,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/abstractions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACxE,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACpF,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DefaultLockManager = exports.DefaultRoomManager = exports.LockManager = exports.RoomManager = exports.TypedEventEmitter = void 0;
|
|
3
|
+
exports.DefaultPresenceManager = exports.PresenceManager = exports.DefaultLockManager = exports.DefaultRoomManager = exports.LockManager = exports.RoomManager = exports.TypedEventEmitter = void 0;
|
|
4
4
|
var EventEmitter_js_1 = require("./EventEmitter.js");
|
|
5
5
|
Object.defineProperty(exports, "TypedEventEmitter", { enumerable: true, get: function () { return EventEmitter_js_1.TypedEventEmitter; } });
|
|
6
6
|
var RoomManager_js_1 = require("./RoomManager.js");
|
|
@@ -11,3 +11,7 @@ var DefaultRoomManager_js_1 = require("./DefaultRoomManager.js");
|
|
|
11
11
|
Object.defineProperty(exports, "DefaultRoomManager", { enumerable: true, get: function () { return DefaultRoomManager_js_1.DefaultRoomManager; } });
|
|
12
12
|
var DefaultLockManager_js_1 = require("./DefaultLockManager.js");
|
|
13
13
|
Object.defineProperty(exports, "DefaultLockManager", { enumerable: true, get: function () { return DefaultLockManager_js_1.DefaultLockManager; } });
|
|
14
|
+
var PresenceManager_js_1 = require("./PresenceManager.js");
|
|
15
|
+
Object.defineProperty(exports, "PresenceManager", { enumerable: true, get: function () { return PresenceManager_js_1.PresenceManager; } });
|
|
16
|
+
var DefaultPresenceManager_js_1 = require("./DefaultPresenceManager.js");
|
|
17
|
+
Object.defineProperty(exports, "DefaultPresenceManager", { enumerable: true, get: function () { return DefaultPresenceManager_js_1.DefaultPresenceManager; } });
|
|
@@ -9,6 +9,7 @@ export declare class MockTransportAdapter implements ITransportAdapter {
|
|
|
9
9
|
private simulateLatency;
|
|
10
10
|
private shouldFailConnection;
|
|
11
11
|
private connectedUrl;
|
|
12
|
+
private connectionAttempts;
|
|
12
13
|
constructor(options?: {
|
|
13
14
|
simulateLatency?: number;
|
|
14
15
|
shouldFailConnection?: boolean;
|
|
@@ -21,11 +22,46 @@ export declare class MockTransportAdapter implements ITransportAdapter {
|
|
|
21
22
|
onClose(handler: CloseHandler): void;
|
|
22
23
|
getState(): ConnectionState;
|
|
23
24
|
isConnected(): boolean;
|
|
24
|
-
simulateMessage(message: Message): void;
|
|
25
|
+
simulateMessage(message: Message, bypassConnectionCheck?: boolean): void;
|
|
25
26
|
simulateError(error: Error): void;
|
|
26
27
|
simulateClose(code: number, reason: string): void;
|
|
27
28
|
getMessageQueue(): Message[];
|
|
28
29
|
clearMessageQueue(): void;
|
|
29
30
|
getConnectedUrl(): string | null;
|
|
31
|
+
getConnectionAttempts(): number;
|
|
32
|
+
resetConnectionAttempts(): void;
|
|
33
|
+
/**
|
|
34
|
+
* Simulate a client connection with authentication state
|
|
35
|
+
* Note: This bypasses the connected state check for testing purposes
|
|
36
|
+
*/
|
|
37
|
+
simulateConnection(clientId: string, options?: {
|
|
38
|
+
authenticated?: boolean;
|
|
39
|
+
userId?: string;
|
|
40
|
+
metadata?: Record<string, unknown>;
|
|
41
|
+
}): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Simulate a client disconnection
|
|
44
|
+
*/
|
|
45
|
+
simulateDisconnect(clientId: string, reason?: string): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Simulate sending a message from a client to the server
|
|
48
|
+
* Properly wraps the message with clientId in payload for server routing
|
|
49
|
+
*
|
|
50
|
+
* @param clientId - The client sending the message
|
|
51
|
+
* @param messageType - The message type (e.g., 'join_room', 'leave_room', 'broadcast_event')
|
|
52
|
+
* @param messagePayload - The message payload
|
|
53
|
+
*/
|
|
54
|
+
simulateClientMessage(clientId: string, messageType: string, messagePayload?: Record<string, unknown>): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Get all messages sent to a specific client
|
|
57
|
+
* This filters the message queue for messages addressed to the given client
|
|
58
|
+
*/
|
|
59
|
+
getSentMessages(clientId: string): any[];
|
|
60
|
+
/**
|
|
61
|
+
* Simulate an incoming message from the server to the client
|
|
62
|
+
* This is used in client tests to mock server responses
|
|
63
|
+
* @param message - The message to send to the client
|
|
64
|
+
*/
|
|
65
|
+
simulateIncomingMessage(message: any): void;
|
|
30
66
|
}
|
|
31
67
|
//# sourceMappingURL=MockTransportAdapter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MockTransportAdapter.d.ts","sourceRoot":"","sources":["../../../src/adapters/mock/MockTransportAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAC;AAChF,OAAO,KAAK,EACV,eAAe,EACf,iBAAiB,EACjB,OAAO,EACP,cAAc,EACd,YAAY,EACZ,YAAY,EACb,MAAM,sBAAsB,CAAC;AAE9B,qBAAa,oBAAqB,YAAW,iBAAiB;IAC5D,OAAO,CAAC,KAAK,CAAmC;IAChD,OAAO,CAAC,eAAe,CAAkC;IACzD,OAAO,CAAC,aAAa,CAAgC;IACrD,OAAO,CAAC,aAAa,CAAgC;IACrD,OAAO,CAAC,YAAY,CAAiB;IACrC,OAAO,CAAC,eAAe,CAAa;IACpC,OAAO,CAAC,oBAAoB,CAAkB;IAC9C,OAAO,CAAC,YAAY,CAAuB;
|
|
1
|
+
{"version":3,"file":"MockTransportAdapter.d.ts","sourceRoot":"","sources":["../../../src/adapters/mock/MockTransportAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAC;AAChF,OAAO,KAAK,EACV,eAAe,EACf,iBAAiB,EACjB,OAAO,EACP,cAAc,EACd,YAAY,EACZ,YAAY,EACb,MAAM,sBAAsB,CAAC;AAE9B,qBAAa,oBAAqB,YAAW,iBAAiB;IAC5D,OAAO,CAAC,KAAK,CAAmC;IAChD,OAAO,CAAC,eAAe,CAAkC;IACzD,OAAO,CAAC,aAAa,CAAgC;IACrD,OAAO,CAAC,aAAa,CAAgC;IACrD,OAAO,CAAC,YAAY,CAAiB;IACrC,OAAO,CAAC,eAAe,CAAa;IACpC,OAAO,CAAC,oBAAoB,CAAkB;IAC9C,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,kBAAkB,CAAa;gBAE3B,OAAO,CAAC,EAAE;QACpB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,oBAAoB,CAAC,EAAE,OAAO,CAAC;KAChC;IAKK,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAqBjE,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAgB3B,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAa3C,SAAS,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI;IAIxC,OAAO,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI;IAIpC,OAAO,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI;IAIpC,QAAQ,IAAI,eAAe;IAI3B,WAAW,IAAI,OAAO;IAKtB,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,qBAAqB,GAAE,OAAe,GAAG,IAAI;IAO/E,aAAa,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAIjC,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAKjD,eAAe,IAAI,OAAO,EAAE;IAI5B,iBAAiB,IAAI,IAAI;IAIzB,eAAe,IAAI,MAAM,GAAG,IAAI;IAIhC,qBAAqB,IAAI,MAAM;IAI/B,uBAAuB,IAAI,IAAI;IAK/B;;;OAGG;IACG,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE;QAClD,aAAa,CAAC,EAAE,OAAO,CAAC;QACxB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KAC/B,GAAG,OAAO,CAAC,IAAI,CAAC;IAsBtB;;OAEG;IACG,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAE,MAAyB,GAAG,OAAO,CAAC,IAAI,CAAC;IAe5F;;;;;;;OAOG;IACG,qBAAqB,CACzB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,cAAc,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAC3C,OAAO,CAAC,IAAI,CAAC;IAehB;;;OAGG;IACH,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,GAAG,EAAE;IAYxC;;;;OAIG;IACH,uBAAuB,CAAC,OAAO,EAAE,GAAG,GAAG,IAAI;CAW5C"}
|