@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.
Files changed (71) hide show
  1. package/README.md +220 -0
  2. package/dist/index.browser.js +1005 -0
  3. package/dist/index.browser.js.map +1 -0
  4. package/dist/index.browser.min.js +3 -0
  5. package/dist/index.browser.min.js.map +1 -0
  6. package/dist/index.cjs +1004 -0
  7. package/dist/index.cjs.map +1 -0
  8. package/dist/index.d.ts +410 -0
  9. package/dist/index.js +1002 -0
  10. package/dist/index.js.map +1 -0
  11. package/package.json +52 -0
  12. package/src/coordinate-utils.d.ts +33 -0
  13. package/src/coordinate-utils.d.ts.map +1 -0
  14. package/src/coordinate-utils.js +70 -0
  15. package/src/coordinate-utils.js.map +1 -0
  16. package/src/coordinate-utils.ts +80 -0
  17. package/src/data-export.d.ts +12 -0
  18. package/src/data-export.d.ts.map +1 -0
  19. package/src/data-export.js +75 -0
  20. package/src/data-export.js.map +1 -0
  21. package/src/data-export.spec.d.ts +2 -0
  22. package/src/data-export.spec.d.ts.map +1 -0
  23. package/src/data-export.spec.js +95 -0
  24. package/src/data-export.spec.js.map +1 -0
  25. package/src/data-export.spec.ts +111 -0
  26. package/src/data-export.ts +84 -0
  27. package/src/data-manager.d.ts +57 -0
  28. package/src/data-manager.d.ts.map +1 -0
  29. package/src/data-manager.js +107 -0
  30. package/src/data-manager.js.map +1 -0
  31. package/src/data-manager.spec.d.ts +2 -0
  32. package/src/data-manager.spec.d.ts.map +1 -0
  33. package/src/data-manager.spec.js +162 -0
  34. package/src/data-manager.spec.js.map +1 -0
  35. package/src/data-manager.spec.ts +195 -0
  36. package/src/data-manager.ts +123 -0
  37. package/src/device-time-sync.d.ts +69 -0
  38. package/src/device-time-sync.d.ts.map +1 -0
  39. package/src/device-time-sync.js +150 -0
  40. package/src/device-time-sync.js.map +1 -0
  41. package/src/device-time-sync.ts +173 -0
  42. package/src/index.d.ts +200 -0
  43. package/src/index.d.ts.map +1 -0
  44. package/src/index.js +431 -0
  45. package/src/index.js.map +1 -0
  46. package/src/index.spec.d.ts +2 -0
  47. package/src/index.spec.d.ts.map +1 -0
  48. package/src/index.spec.js +212 -0
  49. package/src/index.spec.js.map +1 -0
  50. package/src/index.spec.ts +257 -0
  51. package/src/index.ts +535 -0
  52. package/src/time-sync.d.ts +39 -0
  53. package/src/time-sync.d.ts.map +1 -0
  54. package/src/time-sync.js +76 -0
  55. package/src/time-sync.js.map +1 -0
  56. package/src/time-sync.ts +91 -0
  57. package/src/types.d.ts +222 -0
  58. package/src/types.d.ts.map +1 -0
  59. package/src/types.js +5 -0
  60. package/src/types.js.map +1 -0
  61. package/src/types.ts +251 -0
  62. package/src/validation.d.ts +25 -0
  63. package/src/validation.d.ts.map +1 -0
  64. package/src/validation.js +54 -0
  65. package/src/validation.js.map +1 -0
  66. package/src/validation.ts +60 -0
  67. package/src/websocket-client.d.ts +55 -0
  68. package/src/websocket-client.d.ts.map +1 -0
  69. package/src/websocket-client.js +189 -0
  70. package/src/websocket-client.js.map +1 -0
  71. 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
+ }