gameglue 4.0.0 → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +275 -275
  3. package/babel.config.cjs +5 -5
  4. package/coverage/auth.js.html +525 -525
  5. package/coverage/base.css +224 -224
  6. package/coverage/block-navigation.js +87 -87
  7. package/coverage/favicon.png +0 -0
  8. package/coverage/index.html +175 -175
  9. package/coverage/index.js.html +309 -309
  10. package/coverage/lcov-report/auth.js.html +525 -525
  11. package/coverage/lcov-report/base.css +224 -224
  12. package/coverage/lcov-report/block-navigation.js +87 -87
  13. package/coverage/lcov-report/favicon.png +0 -0
  14. package/coverage/lcov-report/index.html +175 -175
  15. package/coverage/lcov-report/index.js.html +309 -309
  16. package/coverage/lcov-report/listener.js.html +528 -528
  17. package/coverage/lcov-report/prettify.css +1 -1
  18. package/coverage/lcov-report/prettify.js +2 -2
  19. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  20. package/coverage/lcov-report/sorter.js +210 -210
  21. package/coverage/lcov-report/user.js.html +117 -117
  22. package/coverage/lcov-report/utils.js.html +117 -117
  23. package/coverage/lcov.info +391 -391
  24. package/coverage/listener.js.html +528 -528
  25. package/coverage/prettify.css +1 -1
  26. package/coverage/prettify.js +2 -2
  27. package/coverage/sort-arrow-sprite.png +0 -0
  28. package/coverage/sorter.js +210 -210
  29. package/coverage/user.js.html +117 -117
  30. package/coverage/utils.js.html +117 -117
  31. package/dist/gg.cjs.js +1 -1
  32. package/dist/gg.cjs.js.map +1 -1
  33. package/dist/gg.esm.js +1 -1
  34. package/dist/gg.esm.js.map +1 -1
  35. package/dist/gg.umd.js +1 -1
  36. package/dist/gg.umd.js.map +1 -1
  37. package/examples/certs/cert.pem +19 -19
  38. package/examples/certs/key.pem +28 -28
  39. package/examples/flight-dashboard.html +431 -431
  40. package/examples/server.js +99 -99
  41. package/examples/telemetry-validator.html +1410 -1410
  42. package/jest.config.cjs +33 -33
  43. package/package.json +56 -56
  44. package/rollup.config.js +57 -57
  45. package/src/auth.js +255 -255
  46. package/src/auth.spec.js +481 -481
  47. package/src/index.js +168 -168
  48. package/src/listener.js +196 -193
  49. package/src/listener.spec.js +598 -598
  50. package/src/presence_listener.js +112 -112
  51. package/src/test/fixtures.js +106 -106
  52. package/src/test/setup.js +51 -51
  53. package/src/utils.js +63 -63
  54. package/src/utils.spec.js +78 -78
  55. package/types/index.d.ts +338 -338
  56. package/webpack.config.js +15 -15
package/src/listener.js CHANGED
@@ -1,193 +1,196 @@
1
- import EventEmitter from 'event-emitter';
2
- import { getGameSchema, normalizeTelemetry } from '@gameglue/schemas';
3
-
4
- export class Listener {
5
- constructor(socket, config) {
6
- this._config = config;
7
- this._socket = socket;
8
- this._callbacks = [];
9
- this._fields = config.fields ? [...config.fields] : null;
10
- this._gameSchema = getGameSchema(config.gameId);
11
- }
12
-
13
- async establishConnection() {
14
- if (!this._socket || !this._config.userId || !this._config.gameId) {
15
- throw new Error('Missing arguments in establishConnection');
16
- }
17
- return new Promise((resolve) => {
18
- // Use object format if fields are specified, otherwise use legacy string format
19
- let listenPayload;
20
- if (this._fields) {
21
- listenPayload = {
22
- userId: this._config.userId,
23
- gameId: this._config.gameId,
24
- fields: this._fields
25
- };
26
- } else {
27
- listenPayload = `${this._config.userId}:${this._config.gameId}`;
28
- }
29
-
30
- this._socket.timeout(5000).emit('listen', listenPayload, (error, response) => {
31
- if (error) {
32
- return resolve({status: 'failed', reason: 'Listen request timed out.'});
33
- }
34
- if (response.status === 'success') {
35
- return resolve({status: 'success'});
36
- } else {
37
- return resolve({status: 'failed', reason: response.reason});
38
- }
39
- });
40
- });
41
- }
42
-
43
- setupEventListener() {
44
- // Listen for telemetry updates
45
- this._socket.on('update', (payload) => {
46
- const rawData = payload?.data;
47
-
48
- // Normalize telemetry to canonical field names
49
- const normalizedData = rawData && this._gameSchema
50
- ? normalizeTelemetry(rawData, this._gameSchema)
51
- : rawData;
52
-
53
- // Apply client-side field filtering on normalized data
54
- let filteredData = normalizedData;
55
- if (this._fields && this._fields.length > 0 && normalizedData) {
56
- filteredData = {};
57
- for (const field of this._fields) {
58
- if (field in normalizedData) {
59
- filteredData[field] = normalizedData[field];
60
- }
61
- }
62
- }
63
-
64
- // Emit with both raw and normalized data
65
- this.emit('update', {
66
- ...payload,
67
- raw: rawData,
68
- data: filteredData
69
- });
70
- });
71
-
72
- // Listen for derived events (landing, takeoff, flight_phase, etc.)
73
- this._socket.on('key-events', (payload) => {
74
- const { gameId, eventType, data } = payload || {};
75
-
76
- // Only emit events for our game
77
- if (gameId !== this._config.gameId) {
78
- return;
79
- }
80
-
81
- // Emit specific event type (landing, takeoff, flight_phase)
82
- if (eventType && data) {
83
- this.emit(eventType, data);
84
- }
85
- });
86
-
87
- return this;
88
- }
89
-
90
- /**
91
- * Subscribe to additional fields dynamically
92
- * @param {string[]} fields - Array of field names to add
93
- * @returns {Promise<{status: string, reason?: string}>}
94
- */
95
- async subscribe(fields) {
96
- if (!Array.isArray(fields) || fields.length === 0) {
97
- throw new Error('fields must be a non-empty array');
98
- }
99
-
100
- // Add new fields to existing list
101
- if (!this._fields) {
102
- this._fields = [...fields];
103
- } else {
104
- for (const field of fields) {
105
- if (!this._fields.includes(field)) {
106
- this._fields.push(field);
107
- }
108
- }
109
- }
110
-
111
- return this._updateSubscription();
112
- }
113
-
114
- /**
115
- * Unsubscribe from specific fields
116
- * @param {string[]} fields - Array of field names to remove
117
- * @returns {Promise<{status: string, reason?: string}>}
118
- */
119
- async unsubscribe(fields) {
120
- if (!Array.isArray(fields) || fields.length === 0) {
121
- throw new Error('fields must be a non-empty array');
122
- }
123
-
124
- if (!this._fields) {
125
- // Currently receiving all fields, create explicit list without these fields
126
- throw new Error('Cannot unsubscribe when receiving all fields. Use subscribe() first to set explicit field list.');
127
- }
128
-
129
- this._fields = this._fields.filter(f => !fields.includes(f));
130
-
131
- return this._updateSubscription();
132
- }
133
-
134
- /**
135
- * Get the current list of subscribed fields
136
- * @returns {string[]|null} - Array of field names, or null if receiving all fields
137
- */
138
- getFields() {
139
- return this._fields ? [...this._fields] : null;
140
- }
141
-
142
- /**
143
- * Send a command to the broadcaster (game client)
144
- * @param {string} command - The canonical command name (e.g., 'gear_up', 'set_autopilot_heading')
145
- * @param {any} value - The value to set (optional for toggle commands)
146
- * @returns {Promise<{status: string, reason?: string}>}
147
- */
148
- async sendCommand(command, value) {
149
- if (!command || typeof command !== 'string') {
150
- throw new Error('command must be a non-empty string');
151
- }
152
-
153
- return new Promise((resolve) => {
154
- const payload = {
155
- userId: this._config.userId,
156
- gameId: this._config.gameId,
157
- data: {
158
- fieldName: command,
159
- value
160
- }
161
- };
162
-
163
- this._socket.timeout(5000).emit('set', payload, (error, response) => {
164
- if (error) {
165
- return resolve({ status: 'failed', reason: 'Command request timed out.' });
166
- }
167
- return resolve(response);
168
- });
169
- });
170
- }
171
-
172
- /**
173
- * Internal method to send subscription update to server
174
- */
175
- async _updateSubscription() {
176
- return new Promise((resolve) => {
177
- const payload = {
178
- userId: this._config.userId,
179
- gameId: this._config.gameId,
180
- fields: this._fields
181
- };
182
-
183
- this._socket.timeout(5000).emit('listen-update', payload, (error, response) => {
184
- if (error) {
185
- return resolve({status: 'failed', reason: 'Update request timed out.'});
186
- }
187
- return resolve(response);
188
- });
189
- });
190
- }
191
- }
192
-
193
- EventEmitter(Listener.prototype);
1
+ import EventEmitter from 'event-emitter';
2
+ import { getGameSchema, normalizeTelemetry } from '@gameglue/schemas';
3
+
4
+ export class Listener {
5
+ constructor(socket, config) {
6
+ this._config = config;
7
+ this._socket = socket;
8
+ this._callbacks = [];
9
+ this._fields = config.fields ? [...config.fields] : null;
10
+ this._gameSchema = getGameSchema(config.gameId);
11
+ }
12
+
13
+ async establishConnection() {
14
+ if (!this._socket || !this._config.userId || !this._config.gameId) {
15
+ throw new Error('Missing arguments in establishConnection');
16
+ }
17
+ return new Promise((resolve) => {
18
+ // Use object format if fields are specified, otherwise use legacy string format
19
+ let listenPayload;
20
+ if (this._fields) {
21
+ listenPayload = {
22
+ userId: this._config.userId,
23
+ gameId: this._config.gameId,
24
+ fields: this._fields
25
+ };
26
+ } else {
27
+ listenPayload = `${this._config.userId}:${this._config.gameId}`;
28
+ }
29
+
30
+ this._socket.timeout(5000).emit('listen', listenPayload, (error, response) => {
31
+ if (error) {
32
+ return resolve({status: 'failed', reason: 'Listen request timed out.'});
33
+ }
34
+ if (response.status === 'success') {
35
+ return resolve({status: 'success'});
36
+ } else {
37
+ return resolve({status: 'failed', reason: response.reason});
38
+ }
39
+ });
40
+ });
41
+ }
42
+
43
+ setupEventListener() {
44
+ // Listen for telemetry updates
45
+ this._socket.on('update', (payload) => {
46
+ // Filter events by gameId when present (multiple listeners share one socket)
47
+ if (payload?.gameId && payload.gameId !== this._config.gameId) return;
48
+
49
+ const rawData = payload?.data;
50
+
51
+ // Normalize telemetry to canonical field names
52
+ const normalizedData = rawData && this._gameSchema
53
+ ? normalizeTelemetry(rawData, this._gameSchema)
54
+ : rawData;
55
+
56
+ // Apply client-side field filtering on normalized data
57
+ let filteredData = normalizedData;
58
+ if (this._fields && this._fields.length > 0 && normalizedData) {
59
+ filteredData = {};
60
+ for (const field of this._fields) {
61
+ if (field in normalizedData) {
62
+ filteredData[field] = normalizedData[field];
63
+ }
64
+ }
65
+ }
66
+
67
+ // Emit with both raw and normalized data
68
+ this.emit('update', {
69
+ ...payload,
70
+ raw: rawData,
71
+ data: filteredData
72
+ });
73
+ });
74
+
75
+ // Listen for derived events (landing, takeoff, flight_phase, etc.)
76
+ this._socket.on('key-events', (payload) => {
77
+ const { gameId, eventType, data } = payload || {};
78
+
79
+ // Only emit events for our game
80
+ if (gameId !== this._config.gameId) {
81
+ return;
82
+ }
83
+
84
+ // Emit specific event type (landing, takeoff, flight_phase)
85
+ if (eventType && data) {
86
+ this.emit(eventType, data);
87
+ }
88
+ });
89
+
90
+ return this;
91
+ }
92
+
93
+ /**
94
+ * Subscribe to additional fields dynamically
95
+ * @param {string[]} fields - Array of field names to add
96
+ * @returns {Promise<{status: string, reason?: string}>}
97
+ */
98
+ async subscribe(fields) {
99
+ if (!Array.isArray(fields) || fields.length === 0) {
100
+ throw new Error('fields must be a non-empty array');
101
+ }
102
+
103
+ // Add new fields to existing list
104
+ if (!this._fields) {
105
+ this._fields = [...fields];
106
+ } else {
107
+ for (const field of fields) {
108
+ if (!this._fields.includes(field)) {
109
+ this._fields.push(field);
110
+ }
111
+ }
112
+ }
113
+
114
+ return this._updateSubscription();
115
+ }
116
+
117
+ /**
118
+ * Unsubscribe from specific fields
119
+ * @param {string[]} fields - Array of field names to remove
120
+ * @returns {Promise<{status: string, reason?: string}>}
121
+ */
122
+ async unsubscribe(fields) {
123
+ if (!Array.isArray(fields) || fields.length === 0) {
124
+ throw new Error('fields must be a non-empty array');
125
+ }
126
+
127
+ if (!this._fields) {
128
+ // Currently receiving all fields, create explicit list without these fields
129
+ throw new Error('Cannot unsubscribe when receiving all fields. Use subscribe() first to set explicit field list.');
130
+ }
131
+
132
+ this._fields = this._fields.filter(f => !fields.includes(f));
133
+
134
+ return this._updateSubscription();
135
+ }
136
+
137
+ /**
138
+ * Get the current list of subscribed fields
139
+ * @returns {string[]|null} - Array of field names, or null if receiving all fields
140
+ */
141
+ getFields() {
142
+ return this._fields ? [...this._fields] : null;
143
+ }
144
+
145
+ /**
146
+ * Send a command to the broadcaster (game client)
147
+ * @param {string} command - The canonical command name (e.g., 'gear_up', 'set_autopilot_heading')
148
+ * @param {any} value - The value to set (optional for toggle commands)
149
+ * @returns {Promise<{status: string, reason?: string}>}
150
+ */
151
+ async sendCommand(command, value) {
152
+ if (!command || typeof command !== 'string') {
153
+ throw new Error('command must be a non-empty string');
154
+ }
155
+
156
+ return new Promise((resolve) => {
157
+ const payload = {
158
+ userId: this._config.userId,
159
+ gameId: this._config.gameId,
160
+ data: {
161
+ fieldName: command,
162
+ value
163
+ }
164
+ };
165
+
166
+ this._socket.timeout(5000).emit('set', payload, (error, response) => {
167
+ if (error) {
168
+ return resolve({ status: 'failed', reason: 'Command request timed out.' });
169
+ }
170
+ return resolve(response);
171
+ });
172
+ });
173
+ }
174
+
175
+ /**
176
+ * Internal method to send subscription update to server
177
+ */
178
+ async _updateSubscription() {
179
+ return new Promise((resolve) => {
180
+ const payload = {
181
+ userId: this._config.userId,
182
+ gameId: this._config.gameId,
183
+ fields: this._fields
184
+ };
185
+
186
+ this._socket.timeout(5000).emit('listen-update', payload, (error, response) => {
187
+ if (error) {
188
+ return resolve({status: 'failed', reason: 'Update request timed out.'});
189
+ }
190
+ return resolve(response);
191
+ });
192
+ });
193
+ }
194
+ }
195
+
196
+ EventEmitter(Listener.prototype);