gameglue 4.0.1 → 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.
- package/LICENSE +21 -21
- package/README.md +275 -275
- package/babel.config.cjs +5 -5
- package/coverage/auth.js.html +525 -525
- package/coverage/base.css +224 -224
- package/coverage/block-navigation.js +87 -87
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +175 -175
- package/coverage/index.js.html +309 -309
- package/coverage/lcov-report/auth.js.html +525 -525
- package/coverage/lcov-report/base.css +224 -224
- package/coverage/lcov-report/block-navigation.js +87 -87
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +175 -175
- package/coverage/lcov-report/index.js.html +309 -309
- package/coverage/lcov-report/listener.js.html +528 -528
- package/coverage/lcov-report/prettify.css +1 -1
- package/coverage/lcov-report/prettify.js +2 -2
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -210
- package/coverage/lcov-report/user.js.html +117 -117
- package/coverage/lcov-report/utils.js.html +117 -117
- package/coverage/lcov.info +391 -391
- package/coverage/listener.js.html +528 -528
- package/coverage/prettify.css +1 -1
- package/coverage/prettify.js +2 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -210
- package/coverage/user.js.html +117 -117
- package/coverage/utils.js.html +117 -117
- package/dist/gg.cjs.js +1 -1
- package/dist/gg.cjs.js.map +1 -1
- package/dist/gg.esm.js +1 -1
- package/dist/gg.esm.js.map +1 -1
- package/dist/gg.umd.js +1 -1
- package/dist/gg.umd.js.map +1 -1
- package/examples/certs/cert.pem +19 -19
- package/examples/certs/key.pem +28 -28
- package/examples/flight-dashboard.html +431 -431
- package/examples/server.js +99 -99
- package/examples/telemetry-validator.html +1410 -1410
- package/jest.config.cjs +33 -33
- package/package.json +56 -56
- package/rollup.config.js +57 -57
- package/src/auth.js +255 -255
- package/src/auth.spec.js +481 -481
- package/src/index.js +168 -168
- package/src/listener.js +196 -196
- package/src/listener.spec.js +598 -598
- package/src/presence_listener.js +112 -112
- package/src/test/fixtures.js +106 -106
- package/src/test/setup.js +51 -51
- package/src/utils.js +63 -63
- package/src/utils.spec.js +78 -78
- package/types/index.d.ts +338 -338
- package/webpack.config.js +15 -15
package/src/presence_listener.js
CHANGED
|
@@ -1,112 +1,112 @@
|
|
|
1
|
-
import EventEmitter from 'event-emitter';
|
|
2
|
-
import { getGameSchema, normalizeTelemetry } from '@gameglue/schemas';
|
|
3
|
-
|
|
4
|
-
export class PresenceListener {
|
|
5
|
-
constructor(socket, config) {
|
|
6
|
-
this._config = config;
|
|
7
|
-
this._socket = socket;
|
|
8
|
-
this._gameSchema = getGameSchema(config.gameId);
|
|
9
|
-
this._broadcasters = [];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
async establishConnection() {
|
|
13
|
-
if (!this._socket || !this._config.clientId || !this._config.gameId) {
|
|
14
|
-
throw new Error('Missing arguments in establishConnection');
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return new Promise((resolve) => {
|
|
18
|
-
const payload = {
|
|
19
|
-
clientId: this._config.clientId,
|
|
20
|
-
gameId: this._config.gameId
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
this._socket.timeout(5000).emit('presenceSubscribe', payload, (error, response) => {
|
|
24
|
-
if (error) {
|
|
25
|
-
return resolve({ status: 'failed', reason: 'Presence request timed out.' });
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (response?.status === 'success') {
|
|
29
|
-
const broadcasters = Array.isArray(response.broadcasters)
|
|
30
|
-
? response.broadcasters.map((snapshot) => this._normalizeSnapshot(snapshot))
|
|
31
|
-
: [];
|
|
32
|
-
this._broadcasters = broadcasters;
|
|
33
|
-
return resolve({ status: 'success', broadcasters });
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return resolve({
|
|
37
|
-
status: 'failed',
|
|
38
|
-
reason: response?.reason || 'Presence subscription failed.'
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
setupEventListener() {
|
|
45
|
-
this._socket.on('presence-update', (snapshot) => {
|
|
46
|
-
const normalized = this._normalizeSnapshot(snapshot);
|
|
47
|
-
this._upsertBroadcaster(normalized);
|
|
48
|
-
this.emit('update', normalized);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
this._socket.on('presence-remove', ({ userId }) => {
|
|
52
|
-
if (!userId) return;
|
|
53
|
-
this._broadcasters = this._broadcasters.filter((b) => b.userId !== userId);
|
|
54
|
-
this.emit('remove', { userId });
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
this._socket.on('connect', () => {
|
|
58
|
-
this.emit('connect');
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
this._socket.on('disconnect', (reason) => {
|
|
62
|
-
this.emit('disconnect', reason);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
return this;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
getBroadcasters() {
|
|
69
|
-
return [...this._broadcasters];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
disconnect() {
|
|
73
|
-
if (this._socket) {
|
|
74
|
-
this._socket.disconnect();
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
_normalizeSnapshot(snapshot) {
|
|
79
|
-
if (!snapshot || typeof snapshot !== 'object') {
|
|
80
|
-
return snapshot;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const rawData = snapshot.data;
|
|
84
|
-
const normalizedData = rawData && this._gameSchema
|
|
85
|
-
? normalizeTelemetry(rawData, this._gameSchema)
|
|
86
|
-
: rawData;
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
...snapshot,
|
|
90
|
-
data: normalizedData,
|
|
91
|
-
raw: rawData
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
_upsertBroadcaster(snapshot) {
|
|
96
|
-
if (!snapshot || !snapshot.userId) {
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const idx = this._broadcasters.findIndex((b) => b.userId === snapshot.userId);
|
|
101
|
-
if (idx >= 0) {
|
|
102
|
-
const next = [...this._broadcasters];
|
|
103
|
-
next[idx] = snapshot;
|
|
104
|
-
this._broadcasters = next;
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
this._broadcasters = [...this._broadcasters, snapshot];
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
EventEmitter(PresenceListener.prototype);
|
|
1
|
+
import EventEmitter from 'event-emitter';
|
|
2
|
+
import { getGameSchema, normalizeTelemetry } from '@gameglue/schemas';
|
|
3
|
+
|
|
4
|
+
export class PresenceListener {
|
|
5
|
+
constructor(socket, config) {
|
|
6
|
+
this._config = config;
|
|
7
|
+
this._socket = socket;
|
|
8
|
+
this._gameSchema = getGameSchema(config.gameId);
|
|
9
|
+
this._broadcasters = [];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async establishConnection() {
|
|
13
|
+
if (!this._socket || !this._config.clientId || !this._config.gameId) {
|
|
14
|
+
throw new Error('Missing arguments in establishConnection');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
const payload = {
|
|
19
|
+
clientId: this._config.clientId,
|
|
20
|
+
gameId: this._config.gameId
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
this._socket.timeout(5000).emit('presenceSubscribe', payload, (error, response) => {
|
|
24
|
+
if (error) {
|
|
25
|
+
return resolve({ status: 'failed', reason: 'Presence request timed out.' });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (response?.status === 'success') {
|
|
29
|
+
const broadcasters = Array.isArray(response.broadcasters)
|
|
30
|
+
? response.broadcasters.map((snapshot) => this._normalizeSnapshot(snapshot))
|
|
31
|
+
: [];
|
|
32
|
+
this._broadcasters = broadcasters;
|
|
33
|
+
return resolve({ status: 'success', broadcasters });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return resolve({
|
|
37
|
+
status: 'failed',
|
|
38
|
+
reason: response?.reason || 'Presence subscription failed.'
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
setupEventListener() {
|
|
45
|
+
this._socket.on('presence-update', (snapshot) => {
|
|
46
|
+
const normalized = this._normalizeSnapshot(snapshot);
|
|
47
|
+
this._upsertBroadcaster(normalized);
|
|
48
|
+
this.emit('update', normalized);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
this._socket.on('presence-remove', ({ userId }) => {
|
|
52
|
+
if (!userId) return;
|
|
53
|
+
this._broadcasters = this._broadcasters.filter((b) => b.userId !== userId);
|
|
54
|
+
this.emit('remove', { userId });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
this._socket.on('connect', () => {
|
|
58
|
+
this.emit('connect');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
this._socket.on('disconnect', (reason) => {
|
|
62
|
+
this.emit('disconnect', reason);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getBroadcasters() {
|
|
69
|
+
return [...this._broadcasters];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
disconnect() {
|
|
73
|
+
if (this._socket) {
|
|
74
|
+
this._socket.disconnect();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_normalizeSnapshot(snapshot) {
|
|
79
|
+
if (!snapshot || typeof snapshot !== 'object') {
|
|
80
|
+
return snapshot;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const rawData = snapshot.data;
|
|
84
|
+
const normalizedData = rawData && this._gameSchema
|
|
85
|
+
? normalizeTelemetry(rawData, this._gameSchema)
|
|
86
|
+
: rawData;
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
...snapshot,
|
|
90
|
+
data: normalizedData,
|
|
91
|
+
raw: rawData
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
_upsertBroadcaster(snapshot) {
|
|
96
|
+
if (!snapshot || !snapshot.userId) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const idx = this._broadcasters.findIndex((b) => b.userId === snapshot.userId);
|
|
101
|
+
if (idx >= 0) {
|
|
102
|
+
const next = [...this._broadcasters];
|
|
103
|
+
next[idx] = snapshot;
|
|
104
|
+
this._broadcasters = next;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this._broadcasters = [...this._broadcasters, snapshot];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
EventEmitter(PresenceListener.prototype);
|
package/src/test/fixtures.js
CHANGED
|
@@ -1,106 +1,106 @@
|
|
|
1
|
-
// Test fixtures for GameGlueJS SDK
|
|
2
|
-
|
|
3
|
-
// Helper to create a JWT with custom payload
|
|
4
|
-
function createMockJwt(payload, expiresInSeconds = 3600) {
|
|
5
|
-
const header = { alg: 'RS256', typ: 'JWT' };
|
|
6
|
-
const now = Math.floor(Date.now() / 1000);
|
|
7
|
-
const defaultPayload = {
|
|
8
|
-
sub: 'user-123',
|
|
9
|
-
exp: now + expiresInSeconds,
|
|
10
|
-
iat: now,
|
|
11
|
-
aud: 'gameglue-client',
|
|
12
|
-
iss: 'https://auth.gameglue.gg/realms/GameGlue',
|
|
13
|
-
scope: 'openid gg:broadcast msfs:read',
|
|
14
|
-
...payload
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const base64Header = Buffer.from(JSON.stringify(header)).toString('base64url');
|
|
18
|
-
const base64Payload = Buffer.from(JSON.stringify(defaultPayload)).toString('base64url');
|
|
19
|
-
// Fake signature (not cryptographically valid, but works for decoding tests)
|
|
20
|
-
const signature = 'fake_signature_for_testing';
|
|
21
|
-
|
|
22
|
-
return `${base64Header}.${base64Payload}.${signature}`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Valid access token (expires in 1 hour)
|
|
26
|
-
const validAccessToken = createMockJwt({ sub: 'user-123' }, 3600);
|
|
27
|
-
|
|
28
|
-
// Expired access token
|
|
29
|
-
const expiredAccessToken = createMockJwt({ sub: 'user-123' }, -100);
|
|
30
|
-
|
|
31
|
-
// Valid refresh token
|
|
32
|
-
const validRefreshToken = createMockJwt({ sub: 'user-123', type: 'refresh' }, 86400);
|
|
33
|
-
|
|
34
|
-
// Mock user configuration
|
|
35
|
-
const mockConfig = {
|
|
36
|
-
clientId: 'test-client-id',
|
|
37
|
-
redirect_uri: 'http://localhost:3000/callback',
|
|
38
|
-
scopes: ['msfs:read', 'gg:broadcast']
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
// Mock listener configuration
|
|
42
|
-
const mockListenerConfig = {
|
|
43
|
-
gameId: 'msfs',
|
|
44
|
-
userId: 'user-123',
|
|
45
|
-
fields: ['altitude', 'airspeed', 'heading']
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
// Mock socket instance
|
|
49
|
-
function createMockSocket() {
|
|
50
|
-
const eventHandlers = {};
|
|
51
|
-
const ioEventHandlers = {};
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
auth: { token: null },
|
|
55
|
-
connected: true,
|
|
56
|
-
on: jest.fn((event, handler) => {
|
|
57
|
-
eventHandlers[event] = handler;
|
|
58
|
-
}),
|
|
59
|
-
emit: jest.fn((event, data, callback) => {
|
|
60
|
-
// Simulate successful responses
|
|
61
|
-
if (callback) {
|
|
62
|
-
if (event === 'listen') {
|
|
63
|
-
callback(null, { status: 'success' });
|
|
64
|
-
} else if (event === 'listen-update') {
|
|
65
|
-
callback(null, { status: 'success' });
|
|
66
|
-
} else if (event === 'set') {
|
|
67
|
-
callback(null, { status: 'success' });
|
|
68
|
-
} else {
|
|
69
|
-
callback(null, { status: 'success' });
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}),
|
|
73
|
-
timeout: jest.fn(function() {
|
|
74
|
-
return this;
|
|
75
|
-
}),
|
|
76
|
-
disconnect: jest.fn(),
|
|
77
|
-
io: {
|
|
78
|
-
on: jest.fn((event, handler) => {
|
|
79
|
-
ioEventHandlers[event] = handler;
|
|
80
|
-
})
|
|
81
|
-
},
|
|
82
|
-
// Helpers for tests to trigger events
|
|
83
|
-
_trigger: (event, data) => {
|
|
84
|
-
if (eventHandlers[event]) {
|
|
85
|
-
eventHandlers[event](data);
|
|
86
|
-
}
|
|
87
|
-
},
|
|
88
|
-
_triggerIo: (event, data) => {
|
|
89
|
-
if (ioEventHandlers[event]) {
|
|
90
|
-
ioEventHandlers[event](data);
|
|
91
|
-
}
|
|
92
|
-
},
|
|
93
|
-
_getHandler: (event) => eventHandlers[event],
|
|
94
|
-
_getIoHandler: (event) => ioEventHandlers[event]
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
module.exports = {
|
|
99
|
-
createMockJwt,
|
|
100
|
-
validAccessToken,
|
|
101
|
-
expiredAccessToken,
|
|
102
|
-
validRefreshToken,
|
|
103
|
-
mockConfig,
|
|
104
|
-
mockListenerConfig,
|
|
105
|
-
createMockSocket
|
|
106
|
-
};
|
|
1
|
+
// Test fixtures for GameGlueJS SDK
|
|
2
|
+
|
|
3
|
+
// Helper to create a JWT with custom payload
|
|
4
|
+
function createMockJwt(payload, expiresInSeconds = 3600) {
|
|
5
|
+
const header = { alg: 'RS256', typ: 'JWT' };
|
|
6
|
+
const now = Math.floor(Date.now() / 1000);
|
|
7
|
+
const defaultPayload = {
|
|
8
|
+
sub: 'user-123',
|
|
9
|
+
exp: now + expiresInSeconds,
|
|
10
|
+
iat: now,
|
|
11
|
+
aud: 'gameglue-client',
|
|
12
|
+
iss: 'https://auth.gameglue.gg/realms/GameGlue',
|
|
13
|
+
scope: 'openid gg:broadcast msfs:read',
|
|
14
|
+
...payload
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const base64Header = Buffer.from(JSON.stringify(header)).toString('base64url');
|
|
18
|
+
const base64Payload = Buffer.from(JSON.stringify(defaultPayload)).toString('base64url');
|
|
19
|
+
// Fake signature (not cryptographically valid, but works for decoding tests)
|
|
20
|
+
const signature = 'fake_signature_for_testing';
|
|
21
|
+
|
|
22
|
+
return `${base64Header}.${base64Payload}.${signature}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Valid access token (expires in 1 hour)
|
|
26
|
+
const validAccessToken = createMockJwt({ sub: 'user-123' }, 3600);
|
|
27
|
+
|
|
28
|
+
// Expired access token
|
|
29
|
+
const expiredAccessToken = createMockJwt({ sub: 'user-123' }, -100);
|
|
30
|
+
|
|
31
|
+
// Valid refresh token
|
|
32
|
+
const validRefreshToken = createMockJwt({ sub: 'user-123', type: 'refresh' }, 86400);
|
|
33
|
+
|
|
34
|
+
// Mock user configuration
|
|
35
|
+
const mockConfig = {
|
|
36
|
+
clientId: 'test-client-id',
|
|
37
|
+
redirect_uri: 'http://localhost:3000/callback',
|
|
38
|
+
scopes: ['msfs:read', 'gg:broadcast']
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Mock listener configuration
|
|
42
|
+
const mockListenerConfig = {
|
|
43
|
+
gameId: 'msfs',
|
|
44
|
+
userId: 'user-123',
|
|
45
|
+
fields: ['altitude', 'airspeed', 'heading']
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Mock socket instance
|
|
49
|
+
function createMockSocket() {
|
|
50
|
+
const eventHandlers = {};
|
|
51
|
+
const ioEventHandlers = {};
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
auth: { token: null },
|
|
55
|
+
connected: true,
|
|
56
|
+
on: jest.fn((event, handler) => {
|
|
57
|
+
eventHandlers[event] = handler;
|
|
58
|
+
}),
|
|
59
|
+
emit: jest.fn((event, data, callback) => {
|
|
60
|
+
// Simulate successful responses
|
|
61
|
+
if (callback) {
|
|
62
|
+
if (event === 'listen') {
|
|
63
|
+
callback(null, { status: 'success' });
|
|
64
|
+
} else if (event === 'listen-update') {
|
|
65
|
+
callback(null, { status: 'success' });
|
|
66
|
+
} else if (event === 'set') {
|
|
67
|
+
callback(null, { status: 'success' });
|
|
68
|
+
} else {
|
|
69
|
+
callback(null, { status: 'success' });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}),
|
|
73
|
+
timeout: jest.fn(function() {
|
|
74
|
+
return this;
|
|
75
|
+
}),
|
|
76
|
+
disconnect: jest.fn(),
|
|
77
|
+
io: {
|
|
78
|
+
on: jest.fn((event, handler) => {
|
|
79
|
+
ioEventHandlers[event] = handler;
|
|
80
|
+
})
|
|
81
|
+
},
|
|
82
|
+
// Helpers for tests to trigger events
|
|
83
|
+
_trigger: (event, data) => {
|
|
84
|
+
if (eventHandlers[event]) {
|
|
85
|
+
eventHandlers[event](data);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
_triggerIo: (event, data) => {
|
|
89
|
+
if (ioEventHandlers[event]) {
|
|
90
|
+
ioEventHandlers[event](data);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
_getHandler: (event) => eventHandlers[event],
|
|
94
|
+
_getIoHandler: (event) => ioEventHandlers[event]
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = {
|
|
99
|
+
createMockJwt,
|
|
100
|
+
validAccessToken,
|
|
101
|
+
expiredAccessToken,
|
|
102
|
+
validRefreshToken,
|
|
103
|
+
mockConfig,
|
|
104
|
+
mockListenerConfig,
|
|
105
|
+
createMockSocket
|
|
106
|
+
};
|
package/src/test/setup.js
CHANGED
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
// Jest setup file for GameGlueJS SDK
|
|
2
|
-
|
|
3
|
-
// Create a proper localStorage mock with internal store
|
|
4
|
-
let localStore = {};
|
|
5
|
-
|
|
6
|
-
const localStorageMock = {
|
|
7
|
-
getItem: jest.fn((key) => localStore[key] || null),
|
|
8
|
-
setItem: jest.fn((key, value) => {
|
|
9
|
-
localStore[key] = String(value);
|
|
10
|
-
}),
|
|
11
|
-
removeItem: jest.fn((key) => {
|
|
12
|
-
delete localStore[key];
|
|
13
|
-
}),
|
|
14
|
-
clear: jest.fn(() => {
|
|
15
|
-
localStore = {};
|
|
16
|
-
}),
|
|
17
|
-
get length() {
|
|
18
|
-
return Object.keys(localStore).length;
|
|
19
|
-
},
|
|
20
|
-
key: jest.fn((i) => Object.keys(localStore)[i] || null)
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
// Override localStorage on window (jsdom provides window)
|
|
24
|
-
Object.defineProperty(window, 'localStorage', {
|
|
25
|
-
value: localStorageMock,
|
|
26
|
-
writable: true
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
// Also set on global for Node environment
|
|
30
|
-
Object.defineProperty(global, 'localStorage', {
|
|
31
|
-
value: localStorageMock,
|
|
32
|
-
writable: true
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
// Mock window.history.pushState and replaceState
|
|
36
|
-
window.history.pushState = jest.fn();
|
|
37
|
-
window.history.replaceState = jest.fn();
|
|
38
|
-
|
|
39
|
-
// Reset mocks and store before each test
|
|
40
|
-
beforeEach(() => {
|
|
41
|
-
localStore = {};
|
|
42
|
-
localStorageMock.getItem.mockClear();
|
|
43
|
-
localStorageMock.setItem.mockClear();
|
|
44
|
-
localStorageMock.removeItem.mockClear();
|
|
45
|
-
localStorageMock.clear.mockClear();
|
|
46
|
-
|
|
47
|
-
// Reset window.location.hash using the proper jsdom approach
|
|
48
|
-
window.location.hash = '';
|
|
49
|
-
|
|
50
|
-
jest.clearAllMocks();
|
|
51
|
-
});
|
|
1
|
+
// Jest setup file for GameGlueJS SDK
|
|
2
|
+
|
|
3
|
+
// Create a proper localStorage mock with internal store
|
|
4
|
+
let localStore = {};
|
|
5
|
+
|
|
6
|
+
const localStorageMock = {
|
|
7
|
+
getItem: jest.fn((key) => localStore[key] || null),
|
|
8
|
+
setItem: jest.fn((key, value) => {
|
|
9
|
+
localStore[key] = String(value);
|
|
10
|
+
}),
|
|
11
|
+
removeItem: jest.fn((key) => {
|
|
12
|
+
delete localStore[key];
|
|
13
|
+
}),
|
|
14
|
+
clear: jest.fn(() => {
|
|
15
|
+
localStore = {};
|
|
16
|
+
}),
|
|
17
|
+
get length() {
|
|
18
|
+
return Object.keys(localStore).length;
|
|
19
|
+
},
|
|
20
|
+
key: jest.fn((i) => Object.keys(localStore)[i] || null)
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Override localStorage on window (jsdom provides window)
|
|
24
|
+
Object.defineProperty(window, 'localStorage', {
|
|
25
|
+
value: localStorageMock,
|
|
26
|
+
writable: true
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Also set on global for Node environment
|
|
30
|
+
Object.defineProperty(global, 'localStorage', {
|
|
31
|
+
value: localStorageMock,
|
|
32
|
+
writable: true
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Mock window.history.pushState and replaceState
|
|
36
|
+
window.history.pushState = jest.fn();
|
|
37
|
+
window.history.replaceState = jest.fn();
|
|
38
|
+
|
|
39
|
+
// Reset mocks and store before each test
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
localStore = {};
|
|
42
|
+
localStorageMock.getItem.mockClear();
|
|
43
|
+
localStorageMock.setItem.mockClear();
|
|
44
|
+
localStorageMock.removeItem.mockClear();
|
|
45
|
+
localStorageMock.clear.mockClear();
|
|
46
|
+
|
|
47
|
+
// Reset window.location.hash using the proper jsdom approach
|
|
48
|
+
window.location.hash = '';
|
|
49
|
+
|
|
50
|
+
jest.clearAllMocks();
|
|
51
|
+
});
|
package/src/utils.js
CHANGED
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
const storageMap = {};
|
|
2
|
-
export const storage = {
|
|
3
|
-
set: (key, value) => {
|
|
4
|
-
return isBrowser() ? localStorage.setItem(key, value) : (storageMap[key] = value);
|
|
5
|
-
},
|
|
6
|
-
get: (key) => {
|
|
7
|
-
return isBrowser() ? localStorage.getItem(key) : storageMap[key];
|
|
8
|
-
},
|
|
9
|
-
remove: (key) => {
|
|
10
|
-
return isBrowser() ? localStorage.removeItem(key) : delete storageMap[key];
|
|
11
|
-
}
|
|
12
|
-
};
|
|
13
|
-
export const isBrowser = () => {
|
|
14
|
-
return !(typeof process === 'object' && String(process) === '[object process]');
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Detect if an error is likely a CORS error.
|
|
19
|
-
* CORS errors in browsers are intentionally vague for security, so we look for common patterns.
|
|
20
|
-
*/
|
|
21
|
-
export function isCorsError(error) {
|
|
22
|
-
if (!error) return false;
|
|
23
|
-
const msg = (error.message || error.toString() || '').toLowerCase();
|
|
24
|
-
|
|
25
|
-
// Common CORS error patterns
|
|
26
|
-
return (
|
|
27
|
-
msg.includes('cors') ||
|
|
28
|
-
msg.includes('cross-origin') ||
|
|
29
|
-
msg.includes('network error') ||
|
|
30
|
-
msg.includes('failed to fetch') ||
|
|
31
|
-
msg.includes('networkerror') ||
|
|
32
|
-
msg.includes('load failed') ||
|
|
33
|
-
// Socket.io specific patterns for CORS failures
|
|
34
|
-
msg.includes('xhr poll error') ||
|
|
35
|
-
msg.includes('websocket error') ||
|
|
36
|
-
(msg.includes('transport') && msg.includes('error'))
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Log helpful CORS debugging guidance to the console.
|
|
42
|
-
*/
|
|
43
|
-
export function logCorsHelp(context, url) {
|
|
44
|
-
console.error(`
|
|
45
|
-
╔══════════════════════════════════════════════════════════════════════════════╗
|
|
46
|
-
║ GameGlue SDK: Possible CORS Error Detected ║
|
|
47
|
-
╠══════════════════════════════════════════════════════════════════════════════╣
|
|
48
|
-
║ Context: ${context.padEnd(66)}║
|
|
49
|
-
║ URL: ${(url || 'unknown').substring(0, 70).padEnd(70)}║
|
|
50
|
-
╠══════════════════════════════════════════════════════════════════════════════╣
|
|
51
|
-
║ This error typically means the server rejected the request due to CORS. ║
|
|
52
|
-
║ ║
|
|
53
|
-
║ To fix this, add your origin to your app's allowed Web Origins: ║
|
|
54
|
-
║ ║
|
|
55
|
-
║ 1. Go to the GameGlue Developer Portal (https://developer.gameglue.gg) ║
|
|
56
|
-
║ 2. Select your app ║
|
|
57
|
-
║ 3. Add your origin to "Web Origins" and save ║
|
|
58
|
-
║ ║
|
|
59
|
-
║ IMPORTANT: Protocol and port must match exactly. ║
|
|
60
|
-
║ http://localhost:3000 and https://localhost:3000 are different origins. ║
|
|
61
|
-
║ http://localhost:3000 and http://localhost:5000 are different origins. ║
|
|
62
|
-
╚══════════════════════════════════════════════════════════════════════════════╝
|
|
63
|
-
`);
|
|
1
|
+
const storageMap = {};
|
|
2
|
+
export const storage = {
|
|
3
|
+
set: (key, value) => {
|
|
4
|
+
return isBrowser() ? localStorage.setItem(key, value) : (storageMap[key] = value);
|
|
5
|
+
},
|
|
6
|
+
get: (key) => {
|
|
7
|
+
return isBrowser() ? localStorage.getItem(key) : storageMap[key];
|
|
8
|
+
},
|
|
9
|
+
remove: (key) => {
|
|
10
|
+
return isBrowser() ? localStorage.removeItem(key) : delete storageMap[key];
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
export const isBrowser = () => {
|
|
14
|
+
return !(typeof process === 'object' && String(process) === '[object process]');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Detect if an error is likely a CORS error.
|
|
19
|
+
* CORS errors in browsers are intentionally vague for security, so we look for common patterns.
|
|
20
|
+
*/
|
|
21
|
+
export function isCorsError(error) {
|
|
22
|
+
if (!error) return false;
|
|
23
|
+
const msg = (error.message || error.toString() || '').toLowerCase();
|
|
24
|
+
|
|
25
|
+
// Common CORS error patterns
|
|
26
|
+
return (
|
|
27
|
+
msg.includes('cors') ||
|
|
28
|
+
msg.includes('cross-origin') ||
|
|
29
|
+
msg.includes('network error') ||
|
|
30
|
+
msg.includes('failed to fetch') ||
|
|
31
|
+
msg.includes('networkerror') ||
|
|
32
|
+
msg.includes('load failed') ||
|
|
33
|
+
// Socket.io specific patterns for CORS failures
|
|
34
|
+
msg.includes('xhr poll error') ||
|
|
35
|
+
msg.includes('websocket error') ||
|
|
36
|
+
(msg.includes('transport') && msg.includes('error'))
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Log helpful CORS debugging guidance to the console.
|
|
42
|
+
*/
|
|
43
|
+
export function logCorsHelp(context, url) {
|
|
44
|
+
console.error(`
|
|
45
|
+
╔══════════════════════════════════════════════════════════════════════════════╗
|
|
46
|
+
║ GameGlue SDK: Possible CORS Error Detected ║
|
|
47
|
+
╠══════════════════════════════════════════════════════════════════════════════╣
|
|
48
|
+
║ Context: ${context.padEnd(66)}║
|
|
49
|
+
║ URL: ${(url || 'unknown').substring(0, 70).padEnd(70)}║
|
|
50
|
+
╠══════════════════════════════════════════════════════════════════════════════╣
|
|
51
|
+
║ This error typically means the server rejected the request due to CORS. ║
|
|
52
|
+
║ ║
|
|
53
|
+
║ To fix this, add your origin to your app's allowed Web Origins: ║
|
|
54
|
+
║ ║
|
|
55
|
+
║ 1. Go to the GameGlue Developer Portal (https://developer.gameglue.gg) ║
|
|
56
|
+
║ 2. Select your app ║
|
|
57
|
+
║ 3. Add your origin to "Web Origins" and save ║
|
|
58
|
+
║ ║
|
|
59
|
+
║ IMPORTANT: Protocol and port must match exactly. ║
|
|
60
|
+
║ http://localhost:3000 and https://localhost:3000 are different origins. ║
|
|
61
|
+
║ http://localhost:3000 and http://localhost:5000 are different origins. ║
|
|
62
|
+
╚══════════════════════════════════════════════════════════════════════════════╝
|
|
63
|
+
`);
|
|
64
64
|
}
|