@jspsych/extension-tobii 0.1.1
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 +220 -0
- package/dist/index.browser.js +1005 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.browser.min.js +3 -0
- package/dist/index.browser.min.js.map +1 -0
- package/dist/index.cjs +1004 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +410 -0
- package/dist/index.js +1002 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
- package/src/coordinate-utils.d.ts +33 -0
- package/src/coordinate-utils.d.ts.map +1 -0
- package/src/coordinate-utils.js +70 -0
- package/src/coordinate-utils.js.map +1 -0
- package/src/coordinate-utils.ts +80 -0
- package/src/data-export.d.ts +12 -0
- package/src/data-export.d.ts.map +1 -0
- package/src/data-export.js +75 -0
- package/src/data-export.js.map +1 -0
- package/src/data-export.spec.d.ts +2 -0
- package/src/data-export.spec.d.ts.map +1 -0
- package/src/data-export.spec.js +95 -0
- package/src/data-export.spec.js.map +1 -0
- package/src/data-export.spec.ts +111 -0
- package/src/data-export.ts +84 -0
- package/src/data-manager.d.ts +57 -0
- package/src/data-manager.d.ts.map +1 -0
- package/src/data-manager.js +107 -0
- package/src/data-manager.js.map +1 -0
- package/src/data-manager.spec.d.ts +2 -0
- package/src/data-manager.spec.d.ts.map +1 -0
- package/src/data-manager.spec.js +162 -0
- package/src/data-manager.spec.js.map +1 -0
- package/src/data-manager.spec.ts +195 -0
- package/src/data-manager.ts +123 -0
- package/src/device-time-sync.d.ts +69 -0
- package/src/device-time-sync.d.ts.map +1 -0
- package/src/device-time-sync.js +150 -0
- package/src/device-time-sync.js.map +1 -0
- package/src/device-time-sync.ts +173 -0
- package/src/index.d.ts +200 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.js +431 -0
- package/src/index.js.map +1 -0
- package/src/index.spec.d.ts +2 -0
- package/src/index.spec.d.ts.map +1 -0
- package/src/index.spec.js +212 -0
- package/src/index.spec.js.map +1 -0
- package/src/index.spec.ts +257 -0
- package/src/index.ts +535 -0
- package/src/time-sync.d.ts +39 -0
- package/src/time-sync.d.ts.map +1 -0
- package/src/time-sync.js +76 -0
- package/src/time-sync.js.map +1 -0
- package/src/time-sync.ts +91 -0
- package/src/types.d.ts +222 -0
- package/src/types.d.ts.map +1 -0
- package/src/types.js +5 -0
- package/src/types.js.map +1 -0
- package/src/types.ts +251 -0
- package/src/validation.d.ts +25 -0
- package/src/validation.d.ts.map +1 -0
- package/src/validation.js +54 -0
- package/src/validation.js.map +1 -0
- package/src/validation.ts +60 -0
- package/src/websocket-client.d.ts +55 -0
- package/src/websocket-client.d.ts.map +1 -0
- package/src/websocket-client.js +189 -0
- package/src/websocket-client.js.map +1 -0
- package/src/websocket-client.ts +227 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data validation utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { GazeData, CalibrationPoint, CalibrationResult, ValidationResult } from './types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Validate gaze data point
|
|
9
|
+
*/
|
|
10
|
+
export function validateGazeData(data: unknown): data is GazeData {
|
|
11
|
+
if (typeof data !== 'object' || data === null) return false;
|
|
12
|
+
const d = data as Record<string, unknown>;
|
|
13
|
+
return (
|
|
14
|
+
typeof d.x === 'number' &&
|
|
15
|
+
typeof d.y === 'number' &&
|
|
16
|
+
typeof d.timestamp === 'number' &&
|
|
17
|
+
!isNaN(d.x) &&
|
|
18
|
+
!isNaN(d.y) &&
|
|
19
|
+
!isNaN(d.timestamp)
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Validate calibration point
|
|
25
|
+
*/
|
|
26
|
+
export function validateCalibrationPoint(point: unknown): point is CalibrationPoint {
|
|
27
|
+
if (typeof point !== 'object' || point === null) return false;
|
|
28
|
+
const p = point as Record<string, unknown>;
|
|
29
|
+
return (
|
|
30
|
+
typeof p.x === 'number' &&
|
|
31
|
+
typeof p.y === 'number' &&
|
|
32
|
+
p.x >= 0 &&
|
|
33
|
+
p.x <= 1 &&
|
|
34
|
+
p.y >= 0 &&
|
|
35
|
+
p.y <= 1
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Filter invalid gaze data
|
|
41
|
+
*/
|
|
42
|
+
export function filterValidGaze(data: GazeData[]): GazeData[] {
|
|
43
|
+
return data.filter(validateGazeData);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Validate that a server response conforms to CalibrationResult
|
|
48
|
+
*/
|
|
49
|
+
export function validateCalibrationResult(data: unknown): data is CalibrationResult {
|
|
50
|
+
if (typeof data !== 'object' || data === null) return false;
|
|
51
|
+
return typeof (data as Record<string, unknown>).success === 'boolean';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validate that a server response conforms to ValidationResult
|
|
56
|
+
*/
|
|
57
|
+
export function validateValidationResult(data: unknown): data is ValidationResult {
|
|
58
|
+
if (typeof data !== 'object' || data === null) return false;
|
|
59
|
+
return typeof (data as Record<string, unknown>).success === 'boolean';
|
|
60
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket client for Tobii server communication
|
|
3
|
+
*/
|
|
4
|
+
import type { ConnectionConfig, WebSocketMessage, ConnectionStatus } from './types';
|
|
5
|
+
export declare class WebSocketClient {
|
|
6
|
+
private ws;
|
|
7
|
+
private config;
|
|
8
|
+
private status;
|
|
9
|
+
private messageHandlers;
|
|
10
|
+
private reconnectTimeout;
|
|
11
|
+
private currentReconnectAttempt;
|
|
12
|
+
private nextRequestId;
|
|
13
|
+
constructor(config?: ConnectionConfig);
|
|
14
|
+
/**
|
|
15
|
+
* Connect to WebSocket server
|
|
16
|
+
*/
|
|
17
|
+
connect(): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Disconnect from WebSocket server
|
|
20
|
+
*/
|
|
21
|
+
disconnect(): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Check if connected
|
|
24
|
+
*/
|
|
25
|
+
isConnected(): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Get connection status
|
|
28
|
+
*/
|
|
29
|
+
getStatus(): ConnectionStatus;
|
|
30
|
+
/**
|
|
31
|
+
* Send message to server
|
|
32
|
+
*/
|
|
33
|
+
send(message: WebSocketMessage): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Send message and wait for response
|
|
36
|
+
*/
|
|
37
|
+
sendAndWait(message: WebSocketMessage, timeout?: number): Promise<Record<string, unknown>>;
|
|
38
|
+
/**
|
|
39
|
+
* Register message handler
|
|
40
|
+
*/
|
|
41
|
+
on(messageType: string, handler: (data: Record<string, unknown>) => void): void;
|
|
42
|
+
/**
|
|
43
|
+
* Unregister message handler
|
|
44
|
+
*/
|
|
45
|
+
off(messageType: string): void;
|
|
46
|
+
/**
|
|
47
|
+
* Handle incoming message
|
|
48
|
+
*/
|
|
49
|
+
private handleMessage;
|
|
50
|
+
/**
|
|
51
|
+
* Handle disconnection and attempt reconnect
|
|
52
|
+
*/
|
|
53
|
+
private handleDisconnect;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=websocket-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket-client.d.ts","sourceRoot":"","sources":["websocket-client.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEpF,qBAAa,eAAe;IAC1B,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,eAAe,CAAuD;IAC9E,OAAO,CAAC,gBAAgB,CAAuB;IAC/C,OAAO,CAAC,uBAAuB,CAAa;IAC5C,OAAO,CAAC,aAAa,CAAa;gBAEtB,MAAM,GAAE,gBAAqB;IAgBzC;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IA4C9B;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAejC;;OAEG;IACH,WAAW,IAAI,OAAO;IAItB;;OAEG;IACH,SAAS,IAAI,gBAAgB;IAI7B;;OAEG;IACG,IAAI,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAQpD;;OAEG;IACG,WAAW,CACf,OAAO,EAAE,gBAAgB,EACzB,OAAO,GAAE,MAAa,GACrB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IA2BnC;;OAEG;IACH,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,GAAG,IAAI;IAS/E;;OAEG;IACH,GAAG,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IAI9B;;OAEG;IACH,OAAO,CAAC,aAAa;IAuBrB;;OAEG;IACH,OAAO,CAAC,gBAAgB;CAwBzB"}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket client for Tobii server communication
|
|
3
|
+
*/
|
|
4
|
+
export class WebSocketClient {
|
|
5
|
+
constructor(config = {}) {
|
|
6
|
+
this.ws = null;
|
|
7
|
+
this.reconnectTimeout = null;
|
|
8
|
+
this.currentReconnectAttempt = 0;
|
|
9
|
+
this.nextRequestId = 0;
|
|
10
|
+
this.config = {
|
|
11
|
+
url: config.url || 'ws://localhost:8080',
|
|
12
|
+
autoConnect: config.autoConnect ?? true,
|
|
13
|
+
reconnectAttempts: config.reconnectAttempts ?? 5,
|
|
14
|
+
reconnectDelay: config.reconnectDelay ?? 1000,
|
|
15
|
+
};
|
|
16
|
+
this.status = {
|
|
17
|
+
connected: false,
|
|
18
|
+
tracking: false,
|
|
19
|
+
};
|
|
20
|
+
this.messageHandlers = new Map();
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Connect to WebSocket server
|
|
24
|
+
*/
|
|
25
|
+
async connect() {
|
|
26
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
try {
|
|
31
|
+
this.ws = new WebSocket(this.config.url);
|
|
32
|
+
// Timeout for connection — cleared on success
|
|
33
|
+
const timeoutId = setTimeout(() => {
|
|
34
|
+
if (this.ws?.readyState !== WebSocket.OPEN) {
|
|
35
|
+
reject(new Error(`Connection timeout (${this.config.url})`));
|
|
36
|
+
}
|
|
37
|
+
}, 5000);
|
|
38
|
+
this.ws.onopen = () => {
|
|
39
|
+
clearTimeout(timeoutId);
|
|
40
|
+
this.status.connected = true;
|
|
41
|
+
this.status.connectedAt = Date.now();
|
|
42
|
+
this.currentReconnectAttempt = 0;
|
|
43
|
+
resolve();
|
|
44
|
+
};
|
|
45
|
+
this.ws.onmessage = (event) => {
|
|
46
|
+
this.handleMessage(event);
|
|
47
|
+
};
|
|
48
|
+
this.ws.onerror = (error) => {
|
|
49
|
+
this.status.lastError = 'WebSocket error';
|
|
50
|
+
console.error('WebSocket error:', error);
|
|
51
|
+
};
|
|
52
|
+
this.ws.onclose = () => {
|
|
53
|
+
this.status.connected = false;
|
|
54
|
+
this.status.tracking = false;
|
|
55
|
+
this.handleDisconnect();
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
reject(error);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Disconnect from WebSocket server
|
|
65
|
+
*/
|
|
66
|
+
async disconnect() {
|
|
67
|
+
if (this.reconnectTimeout !== null) {
|
|
68
|
+
clearTimeout(this.reconnectTimeout);
|
|
69
|
+
this.reconnectTimeout = null;
|
|
70
|
+
}
|
|
71
|
+
if (this.ws) {
|
|
72
|
+
this.ws.close();
|
|
73
|
+
this.ws = null;
|
|
74
|
+
}
|
|
75
|
+
this.status.connected = false;
|
|
76
|
+
this.status.tracking = false;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Check if connected
|
|
80
|
+
*/
|
|
81
|
+
isConnected() {
|
|
82
|
+
return this.ws?.readyState === WebSocket.OPEN;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Get connection status
|
|
86
|
+
*/
|
|
87
|
+
getStatus() {
|
|
88
|
+
return { ...this.status };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Send message to server
|
|
92
|
+
*/
|
|
93
|
+
async send(message) {
|
|
94
|
+
if (!this.isConnected()) {
|
|
95
|
+
throw new Error('Not connected to server');
|
|
96
|
+
}
|
|
97
|
+
this.ws.send(JSON.stringify(message));
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Send message and wait for response
|
|
101
|
+
*/
|
|
102
|
+
async sendAndWait(message, timeout = 5000) {
|
|
103
|
+
if (!this.isConnected()) {
|
|
104
|
+
throw new Error('Not connected to server');
|
|
105
|
+
}
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
// Generate unique ID for this request
|
|
108
|
+
const requestId = `req_${this.nextRequestId++}`;
|
|
109
|
+
const messageWithId = { ...message, requestId };
|
|
110
|
+
// Set up response handler
|
|
111
|
+
const timeoutId = setTimeout(() => {
|
|
112
|
+
this.messageHandlers.delete(requestId);
|
|
113
|
+
reject(new Error('Request timeout'));
|
|
114
|
+
}, timeout);
|
|
115
|
+
this.messageHandlers.set(requestId, (data) => {
|
|
116
|
+
clearTimeout(timeoutId);
|
|
117
|
+
this.messageHandlers.delete(requestId);
|
|
118
|
+
resolve(data);
|
|
119
|
+
});
|
|
120
|
+
// Send message
|
|
121
|
+
this.ws.send(JSON.stringify(messageWithId));
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Register message handler
|
|
126
|
+
*/
|
|
127
|
+
on(messageType, handler) {
|
|
128
|
+
if (this.messageHandlers.has(messageType)) {
|
|
129
|
+
console.warn(`Tobii WebSocket: Overwriting existing handler for message type "${messageType}"`);
|
|
130
|
+
}
|
|
131
|
+
this.messageHandlers.set(messageType, handler);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Unregister message handler
|
|
135
|
+
*/
|
|
136
|
+
off(messageType) {
|
|
137
|
+
this.messageHandlers.delete(messageType);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Handle incoming message
|
|
141
|
+
*/
|
|
142
|
+
handleMessage(event) {
|
|
143
|
+
try {
|
|
144
|
+
const receiveTime = performance.now();
|
|
145
|
+
const data = JSON.parse(event.data);
|
|
146
|
+
data._clientReceiveTime = receiveTime;
|
|
147
|
+
// Check for request ID (response to sendAndWait)
|
|
148
|
+
if (data.requestId && this.messageHandlers.has(data.requestId)) {
|
|
149
|
+
const handler = this.messageHandlers.get(data.requestId);
|
|
150
|
+
handler(data);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// Handle by message type
|
|
154
|
+
if (data.type && this.messageHandlers.has(data.type)) {
|
|
155
|
+
const handler = this.messageHandlers.get(data.type);
|
|
156
|
+
handler(data);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
console.error('Error handling message:', error);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Handle disconnection and attempt reconnect
|
|
165
|
+
*/
|
|
166
|
+
handleDisconnect() {
|
|
167
|
+
if (this.currentReconnectAttempt < this.config.reconnectAttempts) {
|
|
168
|
+
this.currentReconnectAttempt++;
|
|
169
|
+
const delay = this.config.reconnectDelay * this.currentReconnectAttempt;
|
|
170
|
+
this.reconnectTimeout = window.setTimeout(async () => {
|
|
171
|
+
try {
|
|
172
|
+
await this.connect();
|
|
173
|
+
// Emit reconnected event so listeners can re-sync time
|
|
174
|
+
const reconnectedHandler = this.messageHandlers.get('reconnected');
|
|
175
|
+
if (reconnectedHandler) {
|
|
176
|
+
reconnectedHandler({ type: 'reconnected' });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
console.warn(`Tobii: Reconnection attempt ${this.currentReconnectAttempt}/${this.config.reconnectAttempts} failed:`, error);
|
|
181
|
+
}
|
|
182
|
+
}, delay);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
this.status.lastError = 'Max reconnection attempts reached';
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=websocket-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket-client.js","sourceRoot":"","sources":["websocket-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket client for Tobii server communication
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ConnectionConfig, WebSocketMessage, ConnectionStatus } from './types';
|
|
6
|
+
|
|
7
|
+
export class WebSocketClient {
|
|
8
|
+
private ws: WebSocket | null = null;
|
|
9
|
+
private config: Required<ConnectionConfig>;
|
|
10
|
+
private status: ConnectionStatus;
|
|
11
|
+
private messageHandlers: Map<string, (data: Record<string, unknown>) => void>;
|
|
12
|
+
private reconnectTimeout: number | null = null;
|
|
13
|
+
private currentReconnectAttempt: number = 0;
|
|
14
|
+
private nextRequestId: number = 0;
|
|
15
|
+
|
|
16
|
+
constructor(config: ConnectionConfig = {}) {
|
|
17
|
+
this.config = {
|
|
18
|
+
url: config.url || 'ws://localhost:8080',
|
|
19
|
+
autoConnect: config.autoConnect ?? true,
|
|
20
|
+
reconnectAttempts: config.reconnectAttempts ?? 5,
|
|
21
|
+
reconnectDelay: config.reconnectDelay ?? 1000,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
this.status = {
|
|
25
|
+
connected: false,
|
|
26
|
+
tracking: false,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
this.messageHandlers = new Map();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Connect to WebSocket server
|
|
34
|
+
*/
|
|
35
|
+
async connect(): Promise<void> {
|
|
36
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
try {
|
|
42
|
+
this.ws = new WebSocket(this.config.url);
|
|
43
|
+
|
|
44
|
+
// Timeout for connection — cleared on success
|
|
45
|
+
const timeoutId = setTimeout(() => {
|
|
46
|
+
if (this.ws?.readyState !== WebSocket.OPEN) {
|
|
47
|
+
reject(new Error(`Connection timeout (${this.config.url})`));
|
|
48
|
+
}
|
|
49
|
+
}, 5000);
|
|
50
|
+
|
|
51
|
+
this.ws.onopen = () => {
|
|
52
|
+
clearTimeout(timeoutId);
|
|
53
|
+
this.status.connected = true;
|
|
54
|
+
this.status.connectedAt = Date.now();
|
|
55
|
+
this.currentReconnectAttempt = 0;
|
|
56
|
+
resolve();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
this.ws.onmessage = (event) => {
|
|
60
|
+
this.handleMessage(event);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
this.ws.onerror = (error) => {
|
|
64
|
+
this.status.lastError = 'WebSocket error';
|
|
65
|
+
console.error('WebSocket error:', error);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
this.ws.onclose = () => {
|
|
69
|
+
this.status.connected = false;
|
|
70
|
+
this.status.tracking = false;
|
|
71
|
+
this.handleDisconnect();
|
|
72
|
+
};
|
|
73
|
+
} catch (error) {
|
|
74
|
+
reject(error);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Disconnect from WebSocket server
|
|
81
|
+
*/
|
|
82
|
+
async disconnect(): Promise<void> {
|
|
83
|
+
if (this.reconnectTimeout !== null) {
|
|
84
|
+
clearTimeout(this.reconnectTimeout);
|
|
85
|
+
this.reconnectTimeout = null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (this.ws) {
|
|
89
|
+
this.ws.close();
|
|
90
|
+
this.ws = null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this.status.connected = false;
|
|
94
|
+
this.status.tracking = false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check if connected
|
|
99
|
+
*/
|
|
100
|
+
isConnected(): boolean {
|
|
101
|
+
return this.ws?.readyState === WebSocket.OPEN;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get connection status
|
|
106
|
+
*/
|
|
107
|
+
getStatus(): ConnectionStatus {
|
|
108
|
+
return { ...this.status };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Send message to server
|
|
113
|
+
*/
|
|
114
|
+
async send(message: WebSocketMessage): Promise<void> {
|
|
115
|
+
if (!this.isConnected()) {
|
|
116
|
+
throw new Error('Not connected to server');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.ws!.send(JSON.stringify(message));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Send message and wait for response
|
|
124
|
+
*/
|
|
125
|
+
async sendAndWait(
|
|
126
|
+
message: WebSocketMessage,
|
|
127
|
+
timeout: number = 5000
|
|
128
|
+
): Promise<Record<string, unknown>> {
|
|
129
|
+
if (!this.isConnected()) {
|
|
130
|
+
throw new Error('Not connected to server');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
// Generate unique ID for this request
|
|
135
|
+
const requestId = `req_${this.nextRequestId++}`;
|
|
136
|
+
const messageWithId = { ...message, requestId };
|
|
137
|
+
|
|
138
|
+
// Set up response handler
|
|
139
|
+
const timeoutId = setTimeout(() => {
|
|
140
|
+
this.messageHandlers.delete(requestId);
|
|
141
|
+
reject(new Error('Request timeout'));
|
|
142
|
+
}, timeout);
|
|
143
|
+
|
|
144
|
+
this.messageHandlers.set(requestId, (data) => {
|
|
145
|
+
clearTimeout(timeoutId);
|
|
146
|
+
this.messageHandlers.delete(requestId);
|
|
147
|
+
resolve(data);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Send message
|
|
151
|
+
this.ws!.send(JSON.stringify(messageWithId));
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Register message handler
|
|
157
|
+
*/
|
|
158
|
+
on(messageType: string, handler: (data: Record<string, unknown>) => void): void {
|
|
159
|
+
if (this.messageHandlers.has(messageType)) {
|
|
160
|
+
console.warn(
|
|
161
|
+
`Tobii WebSocket: Overwriting existing handler for message type "${messageType}"`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
this.messageHandlers.set(messageType, handler);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Unregister message handler
|
|
169
|
+
*/
|
|
170
|
+
off(messageType: string): void {
|
|
171
|
+
this.messageHandlers.delete(messageType);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Handle incoming message
|
|
176
|
+
*/
|
|
177
|
+
private handleMessage(event: MessageEvent): void {
|
|
178
|
+
try {
|
|
179
|
+
const receiveTime = performance.now();
|
|
180
|
+
const data = JSON.parse(event.data);
|
|
181
|
+
data._clientReceiveTime = receiveTime;
|
|
182
|
+
|
|
183
|
+
// Check for request ID (response to sendAndWait)
|
|
184
|
+
if (data.requestId && this.messageHandlers.has(data.requestId)) {
|
|
185
|
+
const handler = this.messageHandlers.get(data.requestId);
|
|
186
|
+
handler!(data);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Handle by message type
|
|
191
|
+
if (data.type && this.messageHandlers.has(data.type)) {
|
|
192
|
+
const handler = this.messageHandlers.get(data.type);
|
|
193
|
+
handler!(data);
|
|
194
|
+
}
|
|
195
|
+
} catch (error) {
|
|
196
|
+
console.error('Error handling message:', error);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Handle disconnection and attempt reconnect
|
|
202
|
+
*/
|
|
203
|
+
private handleDisconnect(): void {
|
|
204
|
+
if (this.currentReconnectAttempt < this.config.reconnectAttempts) {
|
|
205
|
+
this.currentReconnectAttempt++;
|
|
206
|
+
const delay = this.config.reconnectDelay * this.currentReconnectAttempt;
|
|
207
|
+
|
|
208
|
+
this.reconnectTimeout = window.setTimeout(async () => {
|
|
209
|
+
try {
|
|
210
|
+
await this.connect();
|
|
211
|
+
// Emit reconnected event so listeners can re-sync time
|
|
212
|
+
const reconnectedHandler = this.messageHandlers.get('reconnected');
|
|
213
|
+
if (reconnectedHandler) {
|
|
214
|
+
reconnectedHandler({ type: 'reconnected' });
|
|
215
|
+
}
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.warn(
|
|
218
|
+
`Tobii: Reconnection attempt ${this.currentReconnectAttempt}/${this.config.reconnectAttempts} failed:`,
|
|
219
|
+
error
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}, delay);
|
|
223
|
+
} else {
|
|
224
|
+
this.status.lastError = 'Max reconnection attempts reached';
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|