@playcademy/sdk 0.9.1-beta.2 → 0.9.1-beta.3
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 +109 -602
- package/dist/index.d.ts +19 -1068
- package/dist/index.js +110 -754
- package/dist/internal.d.ts +2021 -7090
- package/dist/internal.js +171 -1163
- package/dist/server/edge.d.ts +9 -359
- package/dist/server.d.ts +9 -359
- package/dist/types.d.ts +550 -4692
- package/package.json +2 -3
package/dist/internal.js
CHANGED
|
@@ -19,13 +19,11 @@ var MessageEvents;
|
|
|
19
19
|
MessageEvents2["RESUME"] = "PLAYCADEMY_RESUME";
|
|
20
20
|
MessageEvents2["FORCE_EXIT"] = "PLAYCADEMY_FORCE_EXIT";
|
|
21
21
|
MessageEvents2["OVERLAY"] = "PLAYCADEMY_OVERLAY";
|
|
22
|
-
MessageEvents2["CONNECTION_STATE"] = "PLAYCADEMY_CONNECTION_STATE";
|
|
23
22
|
MessageEvents2["READY"] = "PLAYCADEMY_READY";
|
|
24
23
|
MessageEvents2["INIT_ERROR"] = "PLAYCADEMY_INIT_ERROR";
|
|
25
24
|
MessageEvents2["EXIT"] = "PLAYCADEMY_EXIT";
|
|
26
25
|
MessageEvents2["TELEMETRY"] = "PLAYCADEMY_TELEMETRY";
|
|
27
26
|
MessageEvents2["KEY_EVENT"] = "PLAYCADEMY_KEY_EVENT";
|
|
28
|
-
MessageEvents2["DISPLAY_ALERT"] = "PLAYCADEMY_DISPLAY_ALERT";
|
|
29
27
|
MessageEvents2["DEMO_END"] = "PLAYCADEMY_DEMO_END";
|
|
30
28
|
MessageEvents2["AUTH_STATE_CHANGE"] = "PLAYCADEMY_AUTH_STATE_CHANGE";
|
|
31
29
|
MessageEvents2["AUTH_CALLBACK"] = "PLAYCADEMY_AUTH_CALLBACK";
|
|
@@ -87,7 +85,6 @@ class PlaycademyMessaging {
|
|
|
87
85
|
"PLAYCADEMY_EXIT" /* EXIT */,
|
|
88
86
|
"PLAYCADEMY_TELEMETRY" /* TELEMETRY */,
|
|
89
87
|
"PLAYCADEMY_KEY_EVENT" /* KEY_EVENT */,
|
|
90
|
-
"PLAYCADEMY_DISPLAY_ALERT" /* DISPLAY_ALERT */,
|
|
91
88
|
"PLAYCADEMY_DEMO_END" /* DEMO_END */
|
|
92
89
|
];
|
|
93
90
|
const shouldUsePostMessage = isIframe && iframeToParentEvents.includes(eventType);
|
|
@@ -194,7 +191,6 @@ function createStandaloneConfig() {
|
|
|
194
191
|
gameUrl: globalThis.location.origin,
|
|
195
192
|
token: "mock-game-token-for-local-dev",
|
|
196
193
|
gameId: "mock-game-id-from-template",
|
|
197
|
-
realtimeUrl: undefined,
|
|
198
194
|
mode: "standalone"
|
|
199
195
|
};
|
|
200
196
|
globalThis.PLAYCADEMY = mockConfig;
|
|
@@ -213,10 +209,7 @@ async function init(options) {
|
|
|
213
209
|
gameUrl: config.gameUrl,
|
|
214
210
|
token: config.token,
|
|
215
211
|
gameId: config.gameId,
|
|
216
|
-
mode: config.mode
|
|
217
|
-
autoStartSession: globalThis.self !== window.top,
|
|
218
|
-
onDisconnect: options?.onDisconnect,
|
|
219
|
-
enableConnectionMonitoring: options?.enableConnectionMonitoring
|
|
212
|
+
mode: config.mode
|
|
220
213
|
});
|
|
221
214
|
client["initPayload"] = config;
|
|
222
215
|
messaging.listen("PLAYCADEMY_TOKEN_REFRESH" /* TOKEN_REFRESH */, ({ token }) => client.setToken(token));
|
|
@@ -637,6 +630,80 @@ function getOAuthConfig(provider) {
|
|
|
637
630
|
var identity = {
|
|
638
631
|
parseOAuthState
|
|
639
632
|
};
|
|
633
|
+
// src/namespaces/game/backend.ts
|
|
634
|
+
function normalizePath(path) {
|
|
635
|
+
return path.startsWith("/") ? path : `/${path}`;
|
|
636
|
+
}
|
|
637
|
+
function createBackendNamespace(client) {
|
|
638
|
+
return {
|
|
639
|
+
async get(path, headers) {
|
|
640
|
+
return client["requestGameBackend"](normalizePath(path), "GET", undefined, headers);
|
|
641
|
+
},
|
|
642
|
+
async post(path, body, headers) {
|
|
643
|
+
return client["requestGameBackend"](normalizePath(path), "POST", body, headers);
|
|
644
|
+
},
|
|
645
|
+
async put(path, body, headers) {
|
|
646
|
+
return client["requestGameBackend"](normalizePath(path), "PUT", body, headers);
|
|
647
|
+
},
|
|
648
|
+
async patch(path, body, headers) {
|
|
649
|
+
return client["requestGameBackend"](normalizePath(path), "PATCH", body, headers);
|
|
650
|
+
},
|
|
651
|
+
async delete(path, headers) {
|
|
652
|
+
return client["requestGameBackend"](normalizePath(path), "DELETE", undefined, headers);
|
|
653
|
+
},
|
|
654
|
+
async request(path, method, body, headers) {
|
|
655
|
+
return client["requestGameBackend"](normalizePath(path), method, body, headers);
|
|
656
|
+
},
|
|
657
|
+
async download(path, method = "GET", body, headers) {
|
|
658
|
+
return client["requestGameBackend"](normalizePath(path), method, body, headers, { raw: true });
|
|
659
|
+
},
|
|
660
|
+
url(pathOrStrings, ...values) {
|
|
661
|
+
if (Array.isArray(pathOrStrings) && "raw" in pathOrStrings) {
|
|
662
|
+
const strings = pathOrStrings;
|
|
663
|
+
const path2 = strings.reduce((acc, str, i) => acc + str + (values[i] != null ? String(values[i]) : ""), "");
|
|
664
|
+
return `${client.gameUrl}/api${path2.startsWith("/") ? path2 : `/${path2}`}`;
|
|
665
|
+
}
|
|
666
|
+
const path = pathOrStrings;
|
|
667
|
+
return `${client.gameUrl}/api${path.startsWith("/") ? path : `/${path}`}`;
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
// src/core/assert.ts
|
|
672
|
+
function assertPlatformMode(client, operation) {
|
|
673
|
+
if (client.mode !== "platform") {
|
|
674
|
+
throw new PlaycademyError(`${operation} requires platform mode (current: ${client.mode}). Check client.mode before calling.`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
function assertDemoMode(client, operation) {
|
|
678
|
+
if (client.mode !== "demo") {
|
|
679
|
+
throw new PlaycademyError(`${operation} requires demo mode (current: ${client.mode}). Check client.mode before calling.`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// src/namespaces/game/demo.ts
|
|
684
|
+
function createDemoNamespace(client) {
|
|
685
|
+
return {
|
|
686
|
+
profile: {
|
|
687
|
+
get: async () => {
|
|
688
|
+
assertDemoMode(client, "demo.profile.get()");
|
|
689
|
+
return client["request"]("/users/demo-profile", "GET");
|
|
690
|
+
},
|
|
691
|
+
update: async (updates) => {
|
|
692
|
+
assertDemoMode(client, "demo.profile.update()");
|
|
693
|
+
return client["request"]("/users/demo-profile", "PATCH", {
|
|
694
|
+
body: updates
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
},
|
|
698
|
+
end: (score, options) => {
|
|
699
|
+
assertDemoMode(client, "demo.end()");
|
|
700
|
+
messaging.send("PLAYCADEMY_DEMO_END" /* DEMO_END */, {
|
|
701
|
+
score,
|
|
702
|
+
...options
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
}
|
|
640
707
|
// src/core/auth/utils.ts
|
|
641
708
|
function openPopupWindow(url, name = "auth-popup", width = 500, height = 600) {
|
|
642
709
|
const left = window.screenX + (window.outerWidth - width) / 2;
|
|
@@ -941,17 +1008,7 @@ function createRuntimeNamespace(client) {
|
|
|
941
1008
|
}
|
|
942
1009
|
return res;
|
|
943
1010
|
},
|
|
944
|
-
exit:
|
|
945
|
-
if (client["internalClientSessionId"] && client["gameId"]) {
|
|
946
|
-
try {
|
|
947
|
-
await client["_sessionManager"].endSession(client["internalClientSessionId"], client["gameId"]);
|
|
948
|
-
} catch (error) {
|
|
949
|
-
log.error("[Playcademy SDK] Failed to auto-end session:", {
|
|
950
|
-
sessionId: client["internalClientSessionId"],
|
|
951
|
-
error
|
|
952
|
-
});
|
|
953
|
-
}
|
|
954
|
-
}
|
|
1011
|
+
exit: () => {
|
|
955
1012
|
messaging.send("PLAYCADEMY_EXIT" /* EXIT */, undefined);
|
|
956
1013
|
},
|
|
957
1014
|
onInit: (handler) => {
|
|
@@ -1051,401 +1108,6 @@ function createAssetsNamespace(client) {
|
|
|
1051
1108
|
}
|
|
1052
1109
|
};
|
|
1053
1110
|
}
|
|
1054
|
-
// src/namespaces/game/backend.ts
|
|
1055
|
-
function normalizePath(path) {
|
|
1056
|
-
return path.startsWith("/") ? path : `/${path}`;
|
|
1057
|
-
}
|
|
1058
|
-
function createBackendNamespace(client) {
|
|
1059
|
-
return {
|
|
1060
|
-
async get(path, headers) {
|
|
1061
|
-
return client["requestGameBackend"](normalizePath(path), "GET", undefined, headers);
|
|
1062
|
-
},
|
|
1063
|
-
async post(path, body, headers) {
|
|
1064
|
-
return client["requestGameBackend"](normalizePath(path), "POST", body, headers);
|
|
1065
|
-
},
|
|
1066
|
-
async put(path, body, headers) {
|
|
1067
|
-
return client["requestGameBackend"](normalizePath(path), "PUT", body, headers);
|
|
1068
|
-
},
|
|
1069
|
-
async patch(path, body, headers) {
|
|
1070
|
-
return client["requestGameBackend"](normalizePath(path), "PATCH", body, headers);
|
|
1071
|
-
},
|
|
1072
|
-
async delete(path, headers) {
|
|
1073
|
-
return client["requestGameBackend"](normalizePath(path), "DELETE", undefined, headers);
|
|
1074
|
-
},
|
|
1075
|
-
async request(path, method, body, headers) {
|
|
1076
|
-
return client["requestGameBackend"](normalizePath(path), method, body, headers);
|
|
1077
|
-
},
|
|
1078
|
-
async download(path, method = "GET", body, headers) {
|
|
1079
|
-
return client["requestGameBackend"](normalizePath(path), method, body, headers, { raw: true });
|
|
1080
|
-
},
|
|
1081
|
-
url(pathOrStrings, ...values) {
|
|
1082
|
-
if (Array.isArray(pathOrStrings) && "raw" in pathOrStrings) {
|
|
1083
|
-
const strings = pathOrStrings;
|
|
1084
|
-
const path2 = strings.reduce((acc, str, i) => acc + str + (values[i] != null ? String(values[i]) : ""), "");
|
|
1085
|
-
return `${client.gameUrl}/api${path2.startsWith("/") ? path2 : `/${path2}`}`;
|
|
1086
|
-
}
|
|
1087
|
-
const path = pathOrStrings;
|
|
1088
|
-
return `${client.gameUrl}/api${path.startsWith("/") ? path : `/${path}`}`;
|
|
1089
|
-
}
|
|
1090
|
-
};
|
|
1091
|
-
}
|
|
1092
|
-
// src/namespaces/game/guard.ts
|
|
1093
|
-
function assertPlatformMode(client, operation) {
|
|
1094
|
-
if (client.mode !== "platform") {
|
|
1095
|
-
throw new PlaycademyError(`${operation} requires platform mode (current: ${client.mode}). Check client.mode before calling.`);
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
function assertDemoMode(client, operation) {
|
|
1099
|
-
if (client.mode !== "demo") {
|
|
1100
|
-
throw new PlaycademyError(`${operation} requires demo mode (current: ${client.mode}). Check client.mode before calling.`);
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
// src/namespaces/game/demo.ts
|
|
1105
|
-
function createDemoNamespace(client) {
|
|
1106
|
-
return {
|
|
1107
|
-
profile: {
|
|
1108
|
-
get: async () => {
|
|
1109
|
-
assertDemoMode(client, "demo.profile.get()");
|
|
1110
|
-
return client["request"]("/users/demo-profile", "GET");
|
|
1111
|
-
},
|
|
1112
|
-
update: async (updates) => {
|
|
1113
|
-
assertDemoMode(client, "demo.profile.update()");
|
|
1114
|
-
return client["request"]("/users/demo-profile", "PATCH", {
|
|
1115
|
-
body: updates
|
|
1116
|
-
});
|
|
1117
|
-
}
|
|
1118
|
-
},
|
|
1119
|
-
end: (score, options) => {
|
|
1120
|
-
assertDemoMode(client, "demo.end()");
|
|
1121
|
-
messaging.send("PLAYCADEMY_DEMO_END" /* DEMO_END */, {
|
|
1122
|
-
score,
|
|
1123
|
-
...options
|
|
1124
|
-
});
|
|
1125
|
-
}
|
|
1126
|
-
};
|
|
1127
|
-
}
|
|
1128
|
-
// src/core/cache/permanent-cache.ts
|
|
1129
|
-
function createPermanentCache(keyPrefix) {
|
|
1130
|
-
const cache = new Map;
|
|
1131
|
-
async function get(key, loader) {
|
|
1132
|
-
const fullKey = keyPrefix ? `${keyPrefix}:${key}` : key;
|
|
1133
|
-
const existing = cache.get(fullKey);
|
|
1134
|
-
if (existing) {
|
|
1135
|
-
return existing;
|
|
1136
|
-
}
|
|
1137
|
-
const promise = loader().catch((error) => {
|
|
1138
|
-
cache.delete(fullKey);
|
|
1139
|
-
throw error;
|
|
1140
|
-
});
|
|
1141
|
-
cache.set(fullKey, promise);
|
|
1142
|
-
return promise;
|
|
1143
|
-
}
|
|
1144
|
-
function clear(key) {
|
|
1145
|
-
if (key === undefined) {
|
|
1146
|
-
cache.clear();
|
|
1147
|
-
} else {
|
|
1148
|
-
const fullKey = keyPrefix ? `${keyPrefix}:${key}` : key;
|
|
1149
|
-
cache.delete(fullKey);
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
function has(key) {
|
|
1153
|
-
const fullKey = keyPrefix ? `${keyPrefix}:${key}` : key;
|
|
1154
|
-
return cache.has(fullKey);
|
|
1155
|
-
}
|
|
1156
|
-
function size() {
|
|
1157
|
-
return cache.size;
|
|
1158
|
-
}
|
|
1159
|
-
function keys() {
|
|
1160
|
-
const result = [];
|
|
1161
|
-
const prefixLen = keyPrefix ? keyPrefix.length + 1 : 0;
|
|
1162
|
-
for (const fullKey of cache.keys()) {
|
|
1163
|
-
result.push(fullKey.substring(prefixLen));
|
|
1164
|
-
}
|
|
1165
|
-
return result;
|
|
1166
|
-
}
|
|
1167
|
-
return { get, clear, has, size, keys };
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
// src/namespaces/game/users.ts
|
|
1171
|
-
function createUsersNamespace(client) {
|
|
1172
|
-
const itemIdCache = createPermanentCache("items");
|
|
1173
|
-
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1174
|
-
async function resolveItemId(identifier) {
|
|
1175
|
-
if (UUID_REGEX.test(identifier)) {
|
|
1176
|
-
return identifier;
|
|
1177
|
-
}
|
|
1178
|
-
const gameId = client["gameId"];
|
|
1179
|
-
const cacheKey = gameId ? `${identifier}:${gameId}` : identifier;
|
|
1180
|
-
return itemIdCache.get(cacheKey, async () => {
|
|
1181
|
-
const queryParams = new URLSearchParams({ slug: identifier });
|
|
1182
|
-
if (gameId) {
|
|
1183
|
-
queryParams.append("gameId", gameId);
|
|
1184
|
-
}
|
|
1185
|
-
const item = await client["request"](`/items/resolve?${queryParams.toString()}`, "GET");
|
|
1186
|
-
return item.id;
|
|
1187
|
-
});
|
|
1188
|
-
}
|
|
1189
|
-
return {
|
|
1190
|
-
me: async () => {
|
|
1191
|
-
assertPlatformMode(client, "users.me()");
|
|
1192
|
-
return client["request"]("/users/me", "GET");
|
|
1193
|
-
},
|
|
1194
|
-
inventory: {
|
|
1195
|
-
get: async () => {
|
|
1196
|
-
assertPlatformMode(client, "users.inventory.get()");
|
|
1197
|
-
return client["request"](`/inventory`, "GET");
|
|
1198
|
-
},
|
|
1199
|
-
add: async (identifier, qty) => {
|
|
1200
|
-
assertPlatformMode(client, "users.inventory.add()");
|
|
1201
|
-
const itemId = await resolveItemId(identifier);
|
|
1202
|
-
const res = await client["request"](`/inventory/add`, "POST", { body: { itemId, qty } });
|
|
1203
|
-
client["emit"]("inventoryChange", {
|
|
1204
|
-
itemId,
|
|
1205
|
-
delta: qty,
|
|
1206
|
-
newTotal: res.newTotal
|
|
1207
|
-
});
|
|
1208
|
-
return res;
|
|
1209
|
-
},
|
|
1210
|
-
remove: async (identifier, qty) => {
|
|
1211
|
-
assertPlatformMode(client, "users.inventory.remove()");
|
|
1212
|
-
const itemId = await resolveItemId(identifier);
|
|
1213
|
-
const res = await client["request"](`/inventory/remove`, "POST", { body: { itemId, qty } });
|
|
1214
|
-
client["emit"]("inventoryChange", {
|
|
1215
|
-
itemId,
|
|
1216
|
-
delta: -qty,
|
|
1217
|
-
newTotal: res.newTotal
|
|
1218
|
-
});
|
|
1219
|
-
return res;
|
|
1220
|
-
},
|
|
1221
|
-
quantity: async (identifier) => {
|
|
1222
|
-
assertPlatformMode(client, "users.inventory.quantity()");
|
|
1223
|
-
const itemId = await resolveItemId(identifier);
|
|
1224
|
-
const inventory = await client["request"](`/inventory`, "GET");
|
|
1225
|
-
const item = inventory.find((inv) => inv.item?.id === itemId);
|
|
1226
|
-
return item?.quantity ?? 0;
|
|
1227
|
-
},
|
|
1228
|
-
has: async (identifier, minQuantity = 1) => {
|
|
1229
|
-
assertPlatformMode(client, "users.inventory.has()");
|
|
1230
|
-
const itemId = await resolveItemId(identifier);
|
|
1231
|
-
const inventory = await client["request"](`/inventory`, "GET");
|
|
1232
|
-
const item = inventory.find((inv) => inv.item?.id === itemId);
|
|
1233
|
-
const qty = item?.quantity ?? 0;
|
|
1234
|
-
return qty >= minQuantity;
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
};
|
|
1238
|
-
}
|
|
1239
|
-
// ../constants/src/achievements.ts
|
|
1240
|
-
var ACHIEVEMENT_IDS = {
|
|
1241
|
-
PLAY_ANY_GAME_DAILY: "play_any_game_daily",
|
|
1242
|
-
DAILY_CHEST_OPEN: "daily_chest_open",
|
|
1243
|
-
LEADERBOARD_TOP3_DAILY: "leaderboard_top3_daily",
|
|
1244
|
-
LEADERBOARD_TOP3_WEEKLY: "leaderboard_top3_weekly",
|
|
1245
|
-
FIRST_SCORE_ANY_GAME: "first_score_any_game",
|
|
1246
|
-
PERSONAL_BEST_ANY_GAME: "personal_best_any_game"
|
|
1247
|
-
};
|
|
1248
|
-
var ACHIEVEMENT_DEFINITIONS = [
|
|
1249
|
-
{
|
|
1250
|
-
id: ACHIEVEMENT_IDS.PLAY_ANY_GAME_DAILY,
|
|
1251
|
-
title: "Play any game",
|
|
1252
|
-
description: "Play any arcade game for at least 60 seconds in a single session.",
|
|
1253
|
-
scope: "daily",
|
|
1254
|
-
rewardCredits: 10,
|
|
1255
|
-
limit: 1,
|
|
1256
|
-
completionType: "time_played_session",
|
|
1257
|
-
completionConfig: { minSeconds: 60 },
|
|
1258
|
-
target: { anyArcadeGame: true },
|
|
1259
|
-
active: true
|
|
1260
|
-
},
|
|
1261
|
-
{
|
|
1262
|
-
id: ACHIEVEMENT_IDS.DAILY_CHEST_OPEN,
|
|
1263
|
-
title: "Opened the daily chest",
|
|
1264
|
-
description: "Find the chest on the map and open it to claim your reward.",
|
|
1265
|
-
scope: "daily",
|
|
1266
|
-
rewardCredits: 10,
|
|
1267
|
-
limit: 1,
|
|
1268
|
-
completionType: "interaction",
|
|
1269
|
-
completionConfig: { triggerId: "bunny_chest" },
|
|
1270
|
-
target: { map: "arcade" },
|
|
1271
|
-
active: true
|
|
1272
|
-
},
|
|
1273
|
-
{
|
|
1274
|
-
id: ACHIEVEMENT_IDS.LEADERBOARD_TOP3_DAILY,
|
|
1275
|
-
title: "Daily Champion",
|
|
1276
|
-
description: "Finish in the top 3 of any game leaderboard for the day.",
|
|
1277
|
-
scope: "daily",
|
|
1278
|
-
rewardCredits: 25,
|
|
1279
|
-
limit: 1,
|
|
1280
|
-
completionType: "leaderboard_rank",
|
|
1281
|
-
completionConfig: {
|
|
1282
|
-
rankRewards: [50, 30, 15]
|
|
1283
|
-
},
|
|
1284
|
-
target: { anyArcadeGame: true },
|
|
1285
|
-
active: true
|
|
1286
|
-
},
|
|
1287
|
-
{
|
|
1288
|
-
id: ACHIEVEMENT_IDS.LEADERBOARD_TOP3_WEEKLY,
|
|
1289
|
-
title: "Weekly Legend",
|
|
1290
|
-
description: "Finish in the top 3 of any game leaderboard for the week.",
|
|
1291
|
-
scope: "weekly",
|
|
1292
|
-
rewardCredits: 50,
|
|
1293
|
-
limit: 1,
|
|
1294
|
-
completionType: "leaderboard_rank",
|
|
1295
|
-
completionConfig: {
|
|
1296
|
-
rankRewards: [100, 60, 30]
|
|
1297
|
-
},
|
|
1298
|
-
target: { anyArcadeGame: true },
|
|
1299
|
-
active: true
|
|
1300
|
-
},
|
|
1301
|
-
{
|
|
1302
|
-
id: ACHIEVEMENT_IDS.FIRST_SCORE_ANY_GAME,
|
|
1303
|
-
title: "First Score",
|
|
1304
|
-
description: "Submit your first score in any arcade game.",
|
|
1305
|
-
scope: "game",
|
|
1306
|
-
rewardCredits: 5,
|
|
1307
|
-
limit: 1,
|
|
1308
|
-
completionType: "first_score",
|
|
1309
|
-
completionConfig: {},
|
|
1310
|
-
target: { anyArcadeGame: true },
|
|
1311
|
-
active: true
|
|
1312
|
-
},
|
|
1313
|
-
{
|
|
1314
|
-
id: ACHIEVEMENT_IDS.PERSONAL_BEST_ANY_GAME,
|
|
1315
|
-
title: "Personal Best",
|
|
1316
|
-
description: "Beat your personal best score in any arcade game.",
|
|
1317
|
-
scope: "daily",
|
|
1318
|
-
rewardCredits: 5,
|
|
1319
|
-
limit: 3,
|
|
1320
|
-
completionType: "personal_best",
|
|
1321
|
-
completionConfig: {},
|
|
1322
|
-
target: { anyArcadeGame: true },
|
|
1323
|
-
active: true
|
|
1324
|
-
}
|
|
1325
|
-
];
|
|
1326
|
-
// ../constants/src/typescript.ts
|
|
1327
|
-
var TypeScriptPackages = {
|
|
1328
|
-
tsc: "tsc",
|
|
1329
|
-
nativePreview: "@typescript/native-preview",
|
|
1330
|
-
nativePreviewBeta: "@typescript/native-preview@beta"
|
|
1331
|
-
};
|
|
1332
|
-
var TYPESCRIPT_RUNNER = {
|
|
1333
|
-
package: TypeScriptPackages.nativePreviewBeta,
|
|
1334
|
-
bin: "tsgo"
|
|
1335
|
-
};
|
|
1336
|
-
// ../constants/src/overworld.ts
|
|
1337
|
-
var ITEM_SLUGS = {
|
|
1338
|
-
PLAYCADEMY_CREDITS: "PLAYCADEMY_CREDITS",
|
|
1339
|
-
PLAYCADEMY_XP: "PLAYCADEMY_XP",
|
|
1340
|
-
FOUNDING_MEMBER_BADGE: "FOUNDING_MEMBER_BADGE",
|
|
1341
|
-
EARLY_ADOPTER_BADGE: "EARLY_ADOPTER_BADGE",
|
|
1342
|
-
FIRST_GAME_BADGE: "FIRST_GAME_BADGE",
|
|
1343
|
-
COMMON_SWORD: "COMMON_SWORD",
|
|
1344
|
-
SMALL_HEALTH_POTION: "SMALL_HEALTH_POTION",
|
|
1345
|
-
SMALL_BACKPACK: "SMALL_BACKPACK",
|
|
1346
|
-
LAVA_LAMP: "LAVA_LAMP",
|
|
1347
|
-
BOOMBOX: "BOOMBOX",
|
|
1348
|
-
CABIN_BED: "CABIN_BED"
|
|
1349
|
-
};
|
|
1350
|
-
var CURRENCIES = {
|
|
1351
|
-
PRIMARY: ITEM_SLUGS.PLAYCADEMY_CREDITS,
|
|
1352
|
-
XP: ITEM_SLUGS.PLAYCADEMY_XP
|
|
1353
|
-
};
|
|
1354
|
-
var BADGES = {
|
|
1355
|
-
FOUNDING_MEMBER: ITEM_SLUGS.FOUNDING_MEMBER_BADGE,
|
|
1356
|
-
EARLY_ADOPTER: ITEM_SLUGS.EARLY_ADOPTER_BADGE,
|
|
1357
|
-
FIRST_GAME: ITEM_SLUGS.FIRST_GAME_BADGE
|
|
1358
|
-
};
|
|
1359
|
-
// ../constants/src/timeback.ts
|
|
1360
|
-
var TIMEBACK_ROUTES = {
|
|
1361
|
-
END_ACTIVITY: "/integrations/timeback/end-activity",
|
|
1362
|
-
GET_XP: "/integrations/timeback/xp",
|
|
1363
|
-
HEARTBEAT: "/integrations/timeback/heartbeat",
|
|
1364
|
-
ADVANCE_COURSE: "/integrations/timeback/advance-course"
|
|
1365
|
-
};
|
|
1366
|
-
// src/core/cache/singleton-cache.ts
|
|
1367
|
-
function createSingletonCache() {
|
|
1368
|
-
let cachedValue;
|
|
1369
|
-
let hasValue = false;
|
|
1370
|
-
async function get(loader) {
|
|
1371
|
-
if (hasValue) {
|
|
1372
|
-
return cachedValue;
|
|
1373
|
-
}
|
|
1374
|
-
const value = await loader();
|
|
1375
|
-
cachedValue = value;
|
|
1376
|
-
hasValue = true;
|
|
1377
|
-
return value;
|
|
1378
|
-
}
|
|
1379
|
-
function clear() {
|
|
1380
|
-
cachedValue = undefined;
|
|
1381
|
-
hasValue = false;
|
|
1382
|
-
}
|
|
1383
|
-
function has() {
|
|
1384
|
-
return hasValue;
|
|
1385
|
-
}
|
|
1386
|
-
return { get, clear, has };
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
// src/namespaces/game/credits.ts
|
|
1390
|
-
function createCreditsNamespace(client) {
|
|
1391
|
-
const creditsIdCache = createSingletonCache();
|
|
1392
|
-
async function getCreditsItemId() {
|
|
1393
|
-
return creditsIdCache.get(async () => {
|
|
1394
|
-
const queryParams = new URLSearchParams({ slug: CURRENCIES.PRIMARY });
|
|
1395
|
-
const creditsItem = await client["request"](`/items/resolve?${queryParams.toString()}`, "GET");
|
|
1396
|
-
if (!creditsItem || !creditsItem.id) {
|
|
1397
|
-
throw new Error("Playcademy Credits item not found in catalog");
|
|
1398
|
-
}
|
|
1399
|
-
return creditsItem.id;
|
|
1400
|
-
});
|
|
1401
|
-
}
|
|
1402
|
-
return {
|
|
1403
|
-
balance: async () => {
|
|
1404
|
-
assertPlatformMode(client, "credits.balance()");
|
|
1405
|
-
const inventory = await client["request"]("/inventory", "GET");
|
|
1406
|
-
const primaryCurrencyInventoryItem = inventory.find((item) => item.item?.slug === CURRENCIES.PRIMARY);
|
|
1407
|
-
return primaryCurrencyInventoryItem?.quantity ?? 0;
|
|
1408
|
-
},
|
|
1409
|
-
add: async (amount) => {
|
|
1410
|
-
assertPlatformMode(client, "credits.add()");
|
|
1411
|
-
if (amount <= 0) {
|
|
1412
|
-
throw new Error("Amount must be positive");
|
|
1413
|
-
}
|
|
1414
|
-
const creditsItemId = await getCreditsItemId();
|
|
1415
|
-
const result = await client["request"]("/inventory/add", "POST", {
|
|
1416
|
-
body: {
|
|
1417
|
-
itemId: creditsItemId,
|
|
1418
|
-
qty: amount
|
|
1419
|
-
}
|
|
1420
|
-
});
|
|
1421
|
-
client["emit"]("inventoryChange", {
|
|
1422
|
-
itemId: creditsItemId,
|
|
1423
|
-
delta: amount,
|
|
1424
|
-
newTotal: result.newTotal
|
|
1425
|
-
});
|
|
1426
|
-
return result.newTotal;
|
|
1427
|
-
},
|
|
1428
|
-
spend: async (amount) => {
|
|
1429
|
-
assertPlatformMode(client, "credits.spend()");
|
|
1430
|
-
if (amount <= 0) {
|
|
1431
|
-
throw new Error("Amount must be positive");
|
|
1432
|
-
}
|
|
1433
|
-
const creditsItemId = await getCreditsItemId();
|
|
1434
|
-
const result = await client["request"]("/inventory/remove", "POST", {
|
|
1435
|
-
body: {
|
|
1436
|
-
itemId: creditsItemId,
|
|
1437
|
-
qty: amount
|
|
1438
|
-
}
|
|
1439
|
-
});
|
|
1440
|
-
client["emit"]("inventoryChange", {
|
|
1441
|
-
itemId: creditsItemId,
|
|
1442
|
-
delta: -amount,
|
|
1443
|
-
newTotal: result.newTotal
|
|
1444
|
-
});
|
|
1445
|
-
return result.newTotal;
|
|
1446
|
-
}
|
|
1447
|
-
};
|
|
1448
|
-
}
|
|
1449
1111
|
// src/namespaces/game/scores.ts
|
|
1450
1112
|
function createScoresNamespace(client) {
|
|
1451
1113
|
return {
|
|
@@ -1460,18 +1122,6 @@ function createScoresNamespace(client) {
|
|
|
1460
1122
|
}
|
|
1461
1123
|
};
|
|
1462
1124
|
}
|
|
1463
|
-
// src/namespaces/game/realtime.ts
|
|
1464
|
-
function createRealtimeNamespace(client) {
|
|
1465
|
-
return {
|
|
1466
|
-
token: {
|
|
1467
|
-
get: async () => {
|
|
1468
|
-
assertPlatformMode(client, "realtime.token.get()");
|
|
1469
|
-
const endpoint = client["gameId"] ? `/games/${client["gameId"]}/realtime/token` : "/realtime/token";
|
|
1470
|
-
return client["request"](endpoint, "POST");
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
};
|
|
1474
|
-
}
|
|
1475
1125
|
// src/core/guards.ts
|
|
1476
1126
|
var VALID_GRADES = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
|
|
1477
1127
|
var VALID_SUBJECTS = [
|
|
@@ -1491,7 +1141,13 @@ function isValidGrade(value) {
|
|
|
1491
1141
|
function isValidSubject(value) {
|
|
1492
1142
|
return typeof value === "string" && VALID_SUBJECTS.includes(value);
|
|
1493
1143
|
}
|
|
1494
|
-
|
|
1144
|
+
// ../constants/src/timeback.ts
|
|
1145
|
+
var TIMEBACK_ROUTES = {
|
|
1146
|
+
END_ACTIVITY: "/integrations/timeback/end-activity",
|
|
1147
|
+
GET_XP: "/integrations/timeback/xp",
|
|
1148
|
+
HEARTBEAT: "/integrations/timeback/heartbeat",
|
|
1149
|
+
ADVANCE_COURSE: "/integrations/timeback/advance-course"
|
|
1150
|
+
};
|
|
1495
1151
|
// src/core/cache/ttl-cache.ts
|
|
1496
1152
|
function createTTLCache(options) {
|
|
1497
1153
|
const cache = new Map;
|
|
@@ -2284,6 +1940,24 @@ function createTimebackNamespace(client) {
|
|
|
2284
1940
|
}
|
|
2285
1941
|
};
|
|
2286
1942
|
}
|
|
1943
|
+
// src/namespaces/game/users.ts
|
|
1944
|
+
function createUsersNamespace(client) {
|
|
1945
|
+
return {
|
|
1946
|
+
me: async () => {
|
|
1947
|
+
assertPlatformMode(client, "users.me()");
|
|
1948
|
+
return client["request"]("/users/me", "GET");
|
|
1949
|
+
}
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
// src/namespaces/platform/admin.ts
|
|
1953
|
+
function createAdminNamespace(client) {
|
|
1954
|
+
return {
|
|
1955
|
+
games: {
|
|
1956
|
+
pauseGame: (gameId) => client["request"](`/admin/games/${gameId}/pause`, "POST"),
|
|
1957
|
+
resumeGame: (gameId) => client["request"](`/admin/games/${gameId}/resume`, "POST")
|
|
1958
|
+
}
|
|
1959
|
+
};
|
|
1960
|
+
}
|
|
2287
1961
|
// src/namespaces/platform/auth.ts
|
|
2288
1962
|
function createAuthNamespace(client) {
|
|
2289
1963
|
return {
|
|
@@ -2331,41 +2005,54 @@ function createAuthNamespace(client) {
|
|
|
2331
2005
|
}
|
|
2332
2006
|
};
|
|
2333
2007
|
}
|
|
2334
|
-
// src/
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
update: (currencyId, props) => client["request"](`/currencies/${currencyId}`, "PATCH", {
|
|
2353
|
-
body: props
|
|
2354
|
-
}),
|
|
2355
|
-
delete: (currencyId) => client["request"](`/currencies/${currencyId}`, "DELETE")
|
|
2356
|
-
},
|
|
2357
|
-
shopListings: {
|
|
2358
|
-
create: (props) => client["request"]("/shop-listings", "POST", { body: props }),
|
|
2359
|
-
get: (listingId) => client["request"](`/shop-listings/${listingId}`, "GET"),
|
|
2360
|
-
list: () => client["request"]("/shop-listings", "GET"),
|
|
2361
|
-
update: (listingId, props) => client["request"](`/shop-listings/${listingId}`, "PATCH", {
|
|
2362
|
-
body: props
|
|
2363
|
-
}),
|
|
2364
|
-
delete: (listingId) => client["request"](`/shop-listings/${listingId}`, "DELETE")
|
|
2008
|
+
// src/core/deploy.ts
|
|
2009
|
+
var S3_ERROR_HINTS = {
|
|
2010
|
+
ExpiredToken: "the AWS credentials used to sign the upload URL have expired",
|
|
2011
|
+
TokenRefreshRequired: "the AWS credentials used to sign the upload URL have expired",
|
|
2012
|
+
AccessDenied: "the presigned URL was rejected, the upload token may have expired",
|
|
2013
|
+
SignatureDoesNotMatch: "upload signature mismatch, likely a Content-Type header discrepancy"
|
|
2014
|
+
};
|
|
2015
|
+
async function parseS3Error(response) {
|
|
2016
|
+
try {
|
|
2017
|
+
const xml = await response.text();
|
|
2018
|
+
const code = xml.match(/<Code>(.+?)<\/Code>/)?.[1];
|
|
2019
|
+
const message = xml.match(/<Message>(.+?)<\/Message>/)?.[1];
|
|
2020
|
+
const hint = code ? S3_ERROR_HINTS[code] : undefined;
|
|
2021
|
+
if (hint) {
|
|
2022
|
+
return `${response.status} (${hint})`;
|
|
2023
|
+
}
|
|
2024
|
+
if (code && message) {
|
|
2025
|
+
return `${response.status} (S3 ${code}: ${message})`;
|
|
2365
2026
|
}
|
|
2366
|
-
|
|
2027
|
+
if (code) {
|
|
2028
|
+
return `${response.status} (S3 error: ${code})`;
|
|
2029
|
+
}
|
|
2030
|
+
return `${response.status} ${response.statusText}`;
|
|
2031
|
+
} catch {
|
|
2032
|
+
return `${response.status} ${response.statusText}`;
|
|
2033
|
+
}
|
|
2367
2034
|
}
|
|
2368
|
-
|
|
2035
|
+
function parseS3ErrorFromXHR(xhr) {
|
|
2036
|
+
try {
|
|
2037
|
+
const xml = xhr.responseText;
|
|
2038
|
+
const code = xml.match(/<Code>(.+?)<\/Code>/)?.[1];
|
|
2039
|
+
const message = xml.match(/<Message>(.+?)<\/Message>/)?.[1];
|
|
2040
|
+
const hint = code ? S3_ERROR_HINTS[code] : undefined;
|
|
2041
|
+
if (hint) {
|
|
2042
|
+
return `${xhr.status} (${hint})`;
|
|
2043
|
+
}
|
|
2044
|
+
if (code && message) {
|
|
2045
|
+
return `${xhr.status} (S3 ${code}: ${message})`;
|
|
2046
|
+
}
|
|
2047
|
+
if (code) {
|
|
2048
|
+
return `${xhr.status} (S3 error: ${code})`;
|
|
2049
|
+
}
|
|
2050
|
+
return `${xhr.status} ${xhr.statusText}`;
|
|
2051
|
+
} catch {
|
|
2052
|
+
return `${xhr.status} ${xhr.statusText}`;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2369
2056
|
class DeployPipeline {
|
|
2370
2057
|
static POLL_INTERVAL_MS = 1000;
|
|
2371
2058
|
static INACTIVITY_TIMEOUT_MS = 60 * 1000;
|
|
@@ -2449,7 +2136,8 @@ class DeployPipeline {
|
|
|
2449
2136
|
headers: { "Content-Type": "application/javascript" }
|
|
2450
2137
|
});
|
|
2451
2138
|
if (!res.ok) {
|
|
2452
|
-
|
|
2139
|
+
const detail = await parseS3Error(res);
|
|
2140
|
+
throw new Error(`Backend code upload failed: ${detail}`);
|
|
2453
2141
|
}
|
|
2454
2142
|
return uploadToken;
|
|
2455
2143
|
}
|
|
@@ -2460,7 +2148,8 @@ class DeployPipeline {
|
|
|
2460
2148
|
headers: { "Content-Type": contentType }
|
|
2461
2149
|
});
|
|
2462
2150
|
if (!res.ok) {
|
|
2463
|
-
|
|
2151
|
+
const detail = await parseS3Error(res);
|
|
2152
|
+
throw new Error(`File upload failed: ${detail}`);
|
|
2464
2153
|
}
|
|
2465
2154
|
}
|
|
2466
2155
|
uploadViaXHR(url, body, contentType, hooks) {
|
|
@@ -2484,7 +2173,8 @@ class DeployPipeline {
|
|
|
2484
2173
|
if (xhr.status >= 200 && xhr.status < 300) {
|
|
2485
2174
|
resolve();
|
|
2486
2175
|
} else {
|
|
2487
|
-
|
|
2176
|
+
const detail = parseS3ErrorFromXHR(xhr);
|
|
2177
|
+
reject(new Error(`File upload failed: ${detail}`));
|
|
2488
2178
|
}
|
|
2489
2179
|
});
|
|
2490
2180
|
xhr.addEventListener("error", () => reject(new Error("File upload failed: network error")));
|
|
@@ -2675,30 +2365,6 @@ function createDevNamespace(client) {
|
|
|
2675
2365
|
logs: {
|
|
2676
2366
|
getToken: async (slug, environment) => client["request"](`/games/${slug}/logs/token`, "POST", { body: { environment } })
|
|
2677
2367
|
}
|
|
2678
|
-
},
|
|
2679
|
-
items: {
|
|
2680
|
-
create: (gameId, slug, itemData) => client["request"](`/games/${gameId}/items`, "POST", {
|
|
2681
|
-
body: {
|
|
2682
|
-
slug,
|
|
2683
|
-
...itemData
|
|
2684
|
-
}
|
|
2685
|
-
}),
|
|
2686
|
-
update: (gameId, itemId, updates) => client["request"](`/games/${gameId}/items/${itemId}`, "PATCH", {
|
|
2687
|
-
body: updates
|
|
2688
|
-
}),
|
|
2689
|
-
list: (gameId) => client["request"](`/games/${gameId}/items`, "GET"),
|
|
2690
|
-
get: (gameId, slug) => {
|
|
2691
|
-
const queryParams = new URLSearchParams({ slug, gameId });
|
|
2692
|
-
return client["request"](`/items/resolve?${queryParams.toString()}`, "GET");
|
|
2693
|
-
},
|
|
2694
|
-
delete: (gameId, itemId) => client["request"](`/games/${gameId}/items/${itemId}`, "DELETE"),
|
|
2695
|
-
shop: {
|
|
2696
|
-
create: (gameId, itemId, listingData) => client["request"](`/games/${gameId}/items/${itemId}/shop-listing`, "POST", { body: listingData }),
|
|
2697
|
-
get: (gameId, itemId) => client["request"](`/games/${gameId}/items/${itemId}/shop-listing`, "GET"),
|
|
2698
|
-
update: (gameId, itemId, updates) => client["request"](`/games/${gameId}/items/${itemId}/shop-listing`, "PATCH", { body: updates }),
|
|
2699
|
-
delete: (gameId, itemId) => client["request"](`/games/${gameId}/items/${itemId}/shop-listing`, "DELETE"),
|
|
2700
|
-
list: (gameId) => client["request"](`/games/${gameId}/shop-listings`, "GET")
|
|
2701
|
-
}
|
|
2702
2368
|
}
|
|
2703
2369
|
};
|
|
2704
2370
|
}
|
|
@@ -2744,17 +2410,6 @@ function createGamesNamespace(client) {
|
|
|
2744
2410
|
search: (gameId, query) => client["request"](`/games/${gameId}/members/search?q=${encodeURIComponent(query)}`, "GET")
|
|
2745
2411
|
},
|
|
2746
2412
|
getSubjects: () => client["request"]("/games/subjects", "GET"),
|
|
2747
|
-
startSession: async (gameId) => {
|
|
2748
|
-
const idToUse = gameId ?? client["_ensureGameId"]();
|
|
2749
|
-
return client["request"](`/games/${idToUse}/sessions`, "POST", {});
|
|
2750
|
-
},
|
|
2751
|
-
endSession: async (sessionId, gameId) => {
|
|
2752
|
-
const effectiveGameIdToEnd = gameId ?? client["_ensureGameId"]();
|
|
2753
|
-
if (client["internalClientSessionId"] && sessionId === client["internalClientSessionId"] && effectiveGameIdToEnd === client["gameId"]) {
|
|
2754
|
-
client["internalClientSessionId"] = undefined;
|
|
2755
|
-
}
|
|
2756
|
-
await client["request"](`/games/${effectiveGameIdToEnd}/sessions/${sessionId}/end`, "POST");
|
|
2757
|
-
},
|
|
2758
2413
|
token: {
|
|
2759
2414
|
create: async (gameId, options) => {
|
|
2760
2415
|
const res = await client["request"](`/games/${gameId}/token`, "POST");
|
|
@@ -2780,83 +2435,6 @@ function createGamesNamespace(client) {
|
|
|
2780
2435
|
}
|
|
2781
2436
|
};
|
|
2782
2437
|
}
|
|
2783
|
-
// src/namespaces/platform/character.ts
|
|
2784
|
-
function createCharacterNamespace(client) {
|
|
2785
|
-
const componentCache = createTTLCache({
|
|
2786
|
-
ttl: 5 * 60 * 1000,
|
|
2787
|
-
keyPrefix: "character.components"
|
|
2788
|
-
});
|
|
2789
|
-
return {
|
|
2790
|
-
get: async (userId) => {
|
|
2791
|
-
try {
|
|
2792
|
-
const path = userId ? `/character/${userId}` : "/character";
|
|
2793
|
-
return await client["request"](path, "GET");
|
|
2794
|
-
} catch (error) {
|
|
2795
|
-
if (error instanceof Error) {
|
|
2796
|
-
if (error.message.includes("404")) {
|
|
2797
|
-
return null;
|
|
2798
|
-
}
|
|
2799
|
-
}
|
|
2800
|
-
throw error;
|
|
2801
|
-
}
|
|
2802
|
-
},
|
|
2803
|
-
create: async (characterData) => client["request"]("/character", "POST", {
|
|
2804
|
-
body: characterData
|
|
2805
|
-
}),
|
|
2806
|
-
update: async (updates) => client["request"]("/character", "PATCH", { body: updates }),
|
|
2807
|
-
components: {
|
|
2808
|
-
list: async (options) => {
|
|
2809
|
-
const cacheKey = options?.level === undefined ? "all" : String(options.level);
|
|
2810
|
-
return componentCache.get(cacheKey, async () => {
|
|
2811
|
-
const path = options?.level !== undefined ? `/character/components?level=${options.level}` : "/character/components";
|
|
2812
|
-
const components = await client["request"](path, "GET");
|
|
2813
|
-
return components || [];
|
|
2814
|
-
}, options);
|
|
2815
|
-
},
|
|
2816
|
-
clearCache: (key) => componentCache.clear(key),
|
|
2817
|
-
getCacheKeys: () => componentCache.getKeys()
|
|
2818
|
-
},
|
|
2819
|
-
accessories: {
|
|
2820
|
-
equip: async (slot, componentId) => client["request"]("/character/accessories/equip", "POST", { body: { slot, accessoryComponentId: componentId } }),
|
|
2821
|
-
remove: async (slot) => client["request"](`/character/accessories/${slot}`, "DELETE"),
|
|
2822
|
-
list: async () => {
|
|
2823
|
-
const character2 = await client.character.get();
|
|
2824
|
-
return character2?.accessories || [];
|
|
2825
|
-
}
|
|
2826
|
-
}
|
|
2827
|
-
};
|
|
2828
|
-
}
|
|
2829
|
-
// src/namespaces/platform/achievements.ts
|
|
2830
|
-
function createAchievementsNamespace(client) {
|
|
2831
|
-
const achievementsListCache = createTTLCache({
|
|
2832
|
-
ttl: 5 * 1000,
|
|
2833
|
-
keyPrefix: "achievements.list"
|
|
2834
|
-
});
|
|
2835
|
-
const achievementsHistoryCache = createTTLCache({
|
|
2836
|
-
ttl: 5 * 1000,
|
|
2837
|
-
keyPrefix: "achievements.history"
|
|
2838
|
-
});
|
|
2839
|
-
return {
|
|
2840
|
-
list: (options) => achievementsListCache.get("current", () => client["request"]("/achievements/current", "GET"), options),
|
|
2841
|
-
history: {
|
|
2842
|
-
list: async (queryOptions, cacheOptions) => {
|
|
2843
|
-
const params = new URLSearchParams;
|
|
2844
|
-
if (queryOptions?.limit) {
|
|
2845
|
-
params.append("limit", String(queryOptions.limit));
|
|
2846
|
-
}
|
|
2847
|
-
const qs = params.toString();
|
|
2848
|
-
const path = qs ? `/achievements/history?${qs}` : "/achievements/history";
|
|
2849
|
-
const cacheKey = qs ? `history-${qs}` : "history";
|
|
2850
|
-
return achievementsHistoryCache.get(cacheKey, () => client["request"](path, "GET"), cacheOptions);
|
|
2851
|
-
}
|
|
2852
|
-
},
|
|
2853
|
-
progress: {
|
|
2854
|
-
submit: async (achievementId) => client["request"]("/achievements/progress", "POST", {
|
|
2855
|
-
body: { achievementId }
|
|
2856
|
-
})
|
|
2857
|
-
}
|
|
2858
|
-
};
|
|
2859
|
-
}
|
|
2860
2438
|
// src/namespaces/platform/leaderboard.ts
|
|
2861
2439
|
async function fetchInternalLeaderboardEntries(client, options) {
|
|
2862
2440
|
const params = new URLSearchParams({
|
|
@@ -2889,215 +2467,6 @@ function createLeaderboardFetchNamespace(client) {
|
|
|
2889
2467
|
fetch: async (options) => fetchPublicLeaderboardEntries(client, options)
|
|
2890
2468
|
};
|
|
2891
2469
|
}
|
|
2892
|
-
// src/core/cache/cooldown-cache.ts
|
|
2893
|
-
function createCooldownCache(defaultCooldownMs) {
|
|
2894
|
-
const lastFetchTime = new Map;
|
|
2895
|
-
const pendingRequests = new Map;
|
|
2896
|
-
const lastResults = new Map;
|
|
2897
|
-
async function get(key, loader, config) {
|
|
2898
|
-
const now = Date.now();
|
|
2899
|
-
const lastFetch = lastFetchTime.get(key) || 0;
|
|
2900
|
-
const timeSinceLastFetch = now - lastFetch;
|
|
2901
|
-
const effectiveCooldown = config?.cooldown !== undefined ? config.cooldown : defaultCooldownMs;
|
|
2902
|
-
const force = config?.force || false;
|
|
2903
|
-
const pending = pendingRequests.get(key);
|
|
2904
|
-
if (pending) {
|
|
2905
|
-
return pending;
|
|
2906
|
-
}
|
|
2907
|
-
if (!force && timeSinceLastFetch < effectiveCooldown) {
|
|
2908
|
-
const cachedResult = lastResults.get(key);
|
|
2909
|
-
if (cachedResult !== undefined) {
|
|
2910
|
-
return Promise.resolve(cachedResult);
|
|
2911
|
-
}
|
|
2912
|
-
}
|
|
2913
|
-
const promise = loader().then((result) => {
|
|
2914
|
-
pendingRequests.delete(key);
|
|
2915
|
-
lastFetchTime.set(key, Date.now());
|
|
2916
|
-
lastResults.set(key, result);
|
|
2917
|
-
return result;
|
|
2918
|
-
}).catch((error) => {
|
|
2919
|
-
pendingRequests.delete(key);
|
|
2920
|
-
throw error;
|
|
2921
|
-
});
|
|
2922
|
-
pendingRequests.set(key, promise);
|
|
2923
|
-
return promise;
|
|
2924
|
-
}
|
|
2925
|
-
function clear(key) {
|
|
2926
|
-
if (key === undefined) {
|
|
2927
|
-
lastFetchTime.clear();
|
|
2928
|
-
pendingRequests.clear();
|
|
2929
|
-
lastResults.clear();
|
|
2930
|
-
} else {
|
|
2931
|
-
lastFetchTime.delete(key);
|
|
2932
|
-
pendingRequests.delete(key);
|
|
2933
|
-
lastResults.delete(key);
|
|
2934
|
-
}
|
|
2935
|
-
}
|
|
2936
|
-
return { get, clear };
|
|
2937
|
-
}
|
|
2938
|
-
|
|
2939
|
-
// src/namespaces/platform/levels.ts
|
|
2940
|
-
function createLevelsNamespace(client) {
|
|
2941
|
-
const progressCache = createCooldownCache(5000);
|
|
2942
|
-
return {
|
|
2943
|
-
get: async () => client["request"]("/users/level", "GET"),
|
|
2944
|
-
progress: async (options) => progressCache.get("user-progress", () => client["request"]("/users/level/progress", "GET"), options),
|
|
2945
|
-
config: {
|
|
2946
|
-
list: async () => client["request"]("/levels/config", "GET"),
|
|
2947
|
-
get: async (level) => client["request"](`/levels/config/${level}`, "GET")
|
|
2948
|
-
}
|
|
2949
|
-
};
|
|
2950
|
-
}
|
|
2951
|
-
// src/namespaces/platform/shop.ts
|
|
2952
|
-
function createShopNamespace(client) {
|
|
2953
|
-
return {
|
|
2954
|
-
view: () => client["request"]("/shop/view", "GET")
|
|
2955
|
-
};
|
|
2956
|
-
}
|
|
2957
|
-
// src/namespaces/platform/notifications.ts
|
|
2958
|
-
function createNotificationsNamespace(client) {
|
|
2959
|
-
const notificationsListCache = createTTLCache({
|
|
2960
|
-
ttl: 5 * 1000,
|
|
2961
|
-
keyPrefix: "notifications.list"
|
|
2962
|
-
});
|
|
2963
|
-
const notificationStatsCache = createTTLCache({
|
|
2964
|
-
ttl: 30 * 1000,
|
|
2965
|
-
keyPrefix: "notifications.stats"
|
|
2966
|
-
});
|
|
2967
|
-
return {
|
|
2968
|
-
list: async (queryOptions, cacheOptions) => {
|
|
2969
|
-
const params = new URLSearchParams;
|
|
2970
|
-
if (queryOptions?.status) {
|
|
2971
|
-
params.append("status", queryOptions.status);
|
|
2972
|
-
}
|
|
2973
|
-
if (queryOptions?.type) {
|
|
2974
|
-
params.append("type", queryOptions.type);
|
|
2975
|
-
}
|
|
2976
|
-
if (queryOptions?.limit) {
|
|
2977
|
-
params.append("limit", String(queryOptions.limit));
|
|
2978
|
-
}
|
|
2979
|
-
if (queryOptions?.offset) {
|
|
2980
|
-
params.append("offset", String(queryOptions.offset));
|
|
2981
|
-
}
|
|
2982
|
-
const qs = params.toString();
|
|
2983
|
-
const path = qs ? `/notifications?${qs}` : "/notifications";
|
|
2984
|
-
const cacheKey = qs ? `list-${qs}` : "list";
|
|
2985
|
-
return notificationsListCache.get(cacheKey, () => client["request"](path, "GET"), cacheOptions);
|
|
2986
|
-
},
|
|
2987
|
-
markAsSeen: async (notificationId) => {
|
|
2988
|
-
const result = await client["request"](`/notifications/${notificationId}`, "PATCH", {
|
|
2989
|
-
body: {
|
|
2990
|
-
id: notificationId,
|
|
2991
|
-
status: "seen"
|
|
2992
|
-
}
|
|
2993
|
-
});
|
|
2994
|
-
notificationsListCache.clear();
|
|
2995
|
-
return result;
|
|
2996
|
-
},
|
|
2997
|
-
markAsClicked: async (notificationId) => {
|
|
2998
|
-
const result = await client["request"](`/notifications/${notificationId}`, "PATCH", {
|
|
2999
|
-
body: {
|
|
3000
|
-
id: notificationId,
|
|
3001
|
-
status: "clicked"
|
|
3002
|
-
}
|
|
3003
|
-
});
|
|
3004
|
-
notificationsListCache.clear();
|
|
3005
|
-
return result;
|
|
3006
|
-
},
|
|
3007
|
-
dismiss: async (notificationId) => {
|
|
3008
|
-
const result = await client["request"](`/notifications/${notificationId}`, "PATCH", {
|
|
3009
|
-
body: {
|
|
3010
|
-
id: notificationId,
|
|
3011
|
-
status: "dismissed"
|
|
3012
|
-
}
|
|
3013
|
-
});
|
|
3014
|
-
notificationsListCache.clear();
|
|
3015
|
-
return result;
|
|
3016
|
-
},
|
|
3017
|
-
markAsDelivered: async (notificationId, method = "realtime") => {
|
|
3018
|
-
const result = await client["request"](`/notifications/${notificationId}`, "PATCH", {
|
|
3019
|
-
body: {
|
|
3020
|
-
id: notificationId,
|
|
3021
|
-
status: "delivered",
|
|
3022
|
-
method
|
|
3023
|
-
}
|
|
3024
|
-
});
|
|
3025
|
-
notificationsListCache.clear();
|
|
3026
|
-
return result;
|
|
3027
|
-
},
|
|
3028
|
-
deliverPending: async () => {
|
|
3029
|
-
const result = await client["request"]("/notifications/deliver", "POST");
|
|
3030
|
-
notificationsListCache.clear();
|
|
3031
|
-
return result;
|
|
3032
|
-
},
|
|
3033
|
-
stats: {
|
|
3034
|
-
get: async (queryOptions, cacheOptions) => {
|
|
3035
|
-
const user = await client.users.me();
|
|
3036
|
-
const params = new URLSearchParams;
|
|
3037
|
-
if (queryOptions?.from) {
|
|
3038
|
-
params.append("from", queryOptions.from);
|
|
3039
|
-
}
|
|
3040
|
-
if (queryOptions?.to) {
|
|
3041
|
-
params.append("to", queryOptions.to);
|
|
3042
|
-
}
|
|
3043
|
-
const qs = params.toString();
|
|
3044
|
-
const path = qs ? `/notifications/stats/${user.id}?${qs}` : `/notifications/stats/${user.id}`;
|
|
3045
|
-
const cacheKey = qs ? `stats-${qs}` : "stats";
|
|
3046
|
-
return notificationStatsCache.get(cacheKey, () => client["request"](path, "GET"), cacheOptions);
|
|
3047
|
-
}
|
|
3048
|
-
}
|
|
3049
|
-
};
|
|
3050
|
-
}
|
|
3051
|
-
// src/namespaces/platform/maps.ts
|
|
3052
|
-
function createMapsNamespace(client) {
|
|
3053
|
-
const mapDataCache = createTTLCache({
|
|
3054
|
-
ttl: 5 * 60 * 1000,
|
|
3055
|
-
keyPrefix: "maps.data"
|
|
3056
|
-
});
|
|
3057
|
-
const mapElementsCache = createTTLCache({
|
|
3058
|
-
ttl: 60 * 1000,
|
|
3059
|
-
keyPrefix: "maps.elements"
|
|
3060
|
-
});
|
|
3061
|
-
return {
|
|
3062
|
-
get: (identifier, options) => mapDataCache.get(identifier, () => client["request"](`/maps/${identifier}`, "GET"), options),
|
|
3063
|
-
elements: (mapId, options) => mapElementsCache.get(mapId, () => client["request"](`/map/elements?mapId=${mapId}`, "GET"), options),
|
|
3064
|
-
objects: {
|
|
3065
|
-
list: (mapId) => client["request"](`/maps/${mapId}/objects`, "GET"),
|
|
3066
|
-
create: (mapId, objectData) => client["request"](`/maps/${mapId}/objects`, "POST", {
|
|
3067
|
-
body: objectData
|
|
3068
|
-
}),
|
|
3069
|
-
delete: (mapId, objectId) => client["request"](`/maps/${mapId}/objects/${objectId}`, "DELETE")
|
|
3070
|
-
}
|
|
3071
|
-
};
|
|
3072
|
-
}
|
|
3073
|
-
// src/namespaces/platform/sprites.ts
|
|
3074
|
-
function createSpritesNamespace(client) {
|
|
3075
|
-
const templateUrlCache = createPermanentCache("sprite-template-urls");
|
|
3076
|
-
return {
|
|
3077
|
-
templates: {
|
|
3078
|
-
get: async (slug) => {
|
|
3079
|
-
if (!slug) {
|
|
3080
|
-
throw new Error("Sprite template slug is required");
|
|
3081
|
-
}
|
|
3082
|
-
const templateMeta = await templateUrlCache.get(slug, async () => client["request"](`/sprites/templates/${slug}`, "GET"));
|
|
3083
|
-
if (!templateMeta.url) {
|
|
3084
|
-
throw new Error(`Template ${slug} has no URL in database`);
|
|
3085
|
-
}
|
|
3086
|
-
const response = await fetch(templateMeta.url);
|
|
3087
|
-
if (!response.ok) {
|
|
3088
|
-
throw new Error(`Failed to fetch template JSON from ${templateMeta.url}: ${response.statusText}`);
|
|
3089
|
-
}
|
|
3090
|
-
return await response.json();
|
|
3091
|
-
}
|
|
3092
|
-
}
|
|
3093
|
-
};
|
|
3094
|
-
}
|
|
3095
|
-
// src/namespaces/platform/telemetry.ts
|
|
3096
|
-
function createTelemetryNamespace(client) {
|
|
3097
|
-
return {
|
|
3098
|
-
pushMetrics: (metrics) => client["request"](`/telemetry/metrics`, "POST", { body: metrics })
|
|
3099
|
-
};
|
|
3100
|
-
}
|
|
3101
2470
|
// src/namespaces/platform/scores.ts
|
|
3102
2471
|
function createScoresNamespace2(client) {
|
|
3103
2472
|
return {
|
|
@@ -3193,50 +2562,6 @@ function createTimebackNamespace2(client) {
|
|
|
3193
2562
|
getIntegrationConfig: (gameId, courseId) => client["request"](`/timeback/integrations/${gameId}/${courseId}`, "GET"),
|
|
3194
2563
|
getConfig: (gameId) => client["request"](`/timeback/config/${gameId}`, "GET")
|
|
3195
2564
|
},
|
|
3196
|
-
xp: {
|
|
3197
|
-
today: async (options) => {
|
|
3198
|
-
const params = new URLSearchParams;
|
|
3199
|
-
if (options?.date) {
|
|
3200
|
-
params.set("date", options.date);
|
|
3201
|
-
}
|
|
3202
|
-
if (options?.timezone) {
|
|
3203
|
-
params.set("tz", options.timezone);
|
|
3204
|
-
}
|
|
3205
|
-
const query = params.toString();
|
|
3206
|
-
const endpoint = query ? `/timeback/xp/today?${query}` : "/timeback/xp/today";
|
|
3207
|
-
return client["request"](endpoint, "GET");
|
|
3208
|
-
},
|
|
3209
|
-
total: async () => client["request"]("/timeback/xp/total", "GET"),
|
|
3210
|
-
history: async (options) => {
|
|
3211
|
-
const params = new URLSearchParams;
|
|
3212
|
-
if (options?.startDate) {
|
|
3213
|
-
params.set("startDate", options.startDate);
|
|
3214
|
-
}
|
|
3215
|
-
if (options?.endDate) {
|
|
3216
|
-
params.set("endDate", options.endDate);
|
|
3217
|
-
}
|
|
3218
|
-
const query = params.toString();
|
|
3219
|
-
const endpoint = query ? `/timeback/xp/history?${query}` : "/timeback/xp/history";
|
|
3220
|
-
return client["request"](endpoint, "GET");
|
|
3221
|
-
},
|
|
3222
|
-
summary: async (options) => {
|
|
3223
|
-
const [today, total] = await Promise.all([
|
|
3224
|
-
client["request"]((() => {
|
|
3225
|
-
const params = new URLSearchParams;
|
|
3226
|
-
if (options?.date) {
|
|
3227
|
-
params.set("date", options.date);
|
|
3228
|
-
}
|
|
3229
|
-
if (options?.timezone) {
|
|
3230
|
-
params.set("tz", options.timezone);
|
|
3231
|
-
}
|
|
3232
|
-
const query = params.toString();
|
|
3233
|
-
return query ? `/timeback/xp/today?${query}` : "/timeback/xp/today";
|
|
3234
|
-
})(), "GET"),
|
|
3235
|
-
client["request"]("/timeback/xp/total", "GET")
|
|
3236
|
-
]);
|
|
3237
|
-
return { today, total };
|
|
3238
|
-
}
|
|
3239
|
-
},
|
|
3240
2565
|
students: {
|
|
3241
2566
|
get: async (timebackId, options) => studentCache.get(timebackId, async () => client["request"](`/timeback/user/${timebackId}`, "GET"), options),
|
|
3242
2567
|
clearCache: (timebackId) => {
|
|
@@ -3389,231 +2714,6 @@ function createAuthStrategy(token, tokenType) {
|
|
|
3389
2714
|
return new GameJwtAuth(token);
|
|
3390
2715
|
}
|
|
3391
2716
|
|
|
3392
|
-
// src/core/connection/monitor.ts
|
|
3393
|
-
class ConnectionMonitor {
|
|
3394
|
-
state = "online";
|
|
3395
|
-
callbacks = new Set;
|
|
3396
|
-
heartbeatInterval;
|
|
3397
|
-
consecutiveFailures = 0;
|
|
3398
|
-
isMonitoring = false;
|
|
3399
|
-
config;
|
|
3400
|
-
constructor(config) {
|
|
3401
|
-
this.config = {
|
|
3402
|
-
baseUrl: config.baseUrl,
|
|
3403
|
-
heartbeatInterval: config.heartbeatInterval ?? 1e4,
|
|
3404
|
-
heartbeatTimeout: config.heartbeatTimeout ?? 5000,
|
|
3405
|
-
failureThreshold: config.failureThreshold ?? 2,
|
|
3406
|
-
enableHeartbeat: config.enableHeartbeat ?? true,
|
|
3407
|
-
enableOfflineEvents: config.enableOfflineEvents ?? true
|
|
3408
|
-
};
|
|
3409
|
-
this._detectInitialState();
|
|
3410
|
-
}
|
|
3411
|
-
start() {
|
|
3412
|
-
if (this.isMonitoring) {
|
|
3413
|
-
return;
|
|
3414
|
-
}
|
|
3415
|
-
this.isMonitoring = true;
|
|
3416
|
-
if (this.config.enableOfflineEvents && typeof globalThis.window !== "undefined") {
|
|
3417
|
-
globalThis.addEventListener("online", this._handleOnline);
|
|
3418
|
-
globalThis.addEventListener("offline", this._handleOffline);
|
|
3419
|
-
}
|
|
3420
|
-
if (this.config.enableHeartbeat) {
|
|
3421
|
-
this._startHeartbeat();
|
|
3422
|
-
}
|
|
3423
|
-
}
|
|
3424
|
-
stop() {
|
|
3425
|
-
if (!this.isMonitoring) {
|
|
3426
|
-
return;
|
|
3427
|
-
}
|
|
3428
|
-
this.isMonitoring = false;
|
|
3429
|
-
if (typeof globalThis.window !== "undefined") {
|
|
3430
|
-
globalThis.removeEventListener("online", this._handleOnline);
|
|
3431
|
-
globalThis.removeEventListener("offline", this._handleOffline);
|
|
3432
|
-
}
|
|
3433
|
-
if (this.heartbeatInterval) {
|
|
3434
|
-
clearInterval(this.heartbeatInterval);
|
|
3435
|
-
this.heartbeatInterval = undefined;
|
|
3436
|
-
}
|
|
3437
|
-
}
|
|
3438
|
-
onChange(callback) {
|
|
3439
|
-
this.callbacks.add(callback);
|
|
3440
|
-
return () => this.callbacks.delete(callback);
|
|
3441
|
-
}
|
|
3442
|
-
getState() {
|
|
3443
|
-
return this.state;
|
|
3444
|
-
}
|
|
3445
|
-
async checkNow() {
|
|
3446
|
-
await this._performHeartbeat();
|
|
3447
|
-
return this.state;
|
|
3448
|
-
}
|
|
3449
|
-
reportRequestFailure(error) {
|
|
3450
|
-
const isNetworkError = error instanceof TypeError || error instanceof Error && error.message.includes("fetch");
|
|
3451
|
-
if (!isNetworkError) {
|
|
3452
|
-
return;
|
|
3453
|
-
}
|
|
3454
|
-
this.consecutiveFailures++;
|
|
3455
|
-
if (this.consecutiveFailures >= this.config.failureThreshold) {
|
|
3456
|
-
this._setState("degraded", "Multiple consecutive request failures");
|
|
3457
|
-
}
|
|
3458
|
-
}
|
|
3459
|
-
reportRequestSuccess() {
|
|
3460
|
-
if (this.consecutiveFailures > 0) {
|
|
3461
|
-
this.consecutiveFailures = 0;
|
|
3462
|
-
if (this.state === "degraded") {
|
|
3463
|
-
this._setState("online", "Requests succeeding again");
|
|
3464
|
-
}
|
|
3465
|
-
}
|
|
3466
|
-
}
|
|
3467
|
-
_detectInitialState() {
|
|
3468
|
-
if (typeof navigator !== "undefined" && !navigator.onLine) {
|
|
3469
|
-
this.state = "offline";
|
|
3470
|
-
}
|
|
3471
|
-
}
|
|
3472
|
-
_handleOnline = () => {
|
|
3473
|
-
this.consecutiveFailures = 0;
|
|
3474
|
-
this._setState("online", "Browser online event");
|
|
3475
|
-
};
|
|
3476
|
-
_handleOffline = () => {
|
|
3477
|
-
this._setState("offline", "Browser offline event");
|
|
3478
|
-
};
|
|
3479
|
-
_startHeartbeat() {
|
|
3480
|
-
this._performHeartbeat();
|
|
3481
|
-
this.heartbeatInterval = setInterval(() => {
|
|
3482
|
-
this._performHeartbeat();
|
|
3483
|
-
}, this.config.heartbeatInterval);
|
|
3484
|
-
}
|
|
3485
|
-
async _performHeartbeat() {
|
|
3486
|
-
if (typeof navigator !== "undefined" && !navigator.onLine) {
|
|
3487
|
-
return;
|
|
3488
|
-
}
|
|
3489
|
-
try {
|
|
3490
|
-
const controller = new AbortController;
|
|
3491
|
-
const timeoutId = setTimeout(() => controller.abort(), this.config.heartbeatTimeout);
|
|
3492
|
-
const response = await fetch(`${this.config.baseUrl}/ping`, {
|
|
3493
|
-
method: "GET",
|
|
3494
|
-
signal: controller.signal,
|
|
3495
|
-
cache: "no-store"
|
|
3496
|
-
});
|
|
3497
|
-
clearTimeout(timeoutId);
|
|
3498
|
-
if (response.ok) {
|
|
3499
|
-
this.consecutiveFailures = 0;
|
|
3500
|
-
if (this.state !== "online") {
|
|
3501
|
-
this._setState("online", "Heartbeat successful");
|
|
3502
|
-
}
|
|
3503
|
-
} else {
|
|
3504
|
-
this._handleHeartbeatFailure("Heartbeat returned non-OK status");
|
|
3505
|
-
}
|
|
3506
|
-
} catch (error) {
|
|
3507
|
-
this._handleHeartbeatFailure(error instanceof Error ? error.message : "Heartbeat failed");
|
|
3508
|
-
}
|
|
3509
|
-
}
|
|
3510
|
-
_handleHeartbeatFailure(reason) {
|
|
3511
|
-
this.consecutiveFailures++;
|
|
3512
|
-
if (this.consecutiveFailures >= this.config.failureThreshold) {
|
|
3513
|
-
if (typeof navigator !== "undefined" && !navigator.onLine) {
|
|
3514
|
-
this._setState("offline", reason);
|
|
3515
|
-
} else {
|
|
3516
|
-
this._setState("degraded", reason);
|
|
3517
|
-
}
|
|
3518
|
-
}
|
|
3519
|
-
}
|
|
3520
|
-
_setState(newState, reason) {
|
|
3521
|
-
if (this.state === newState) {
|
|
3522
|
-
return;
|
|
3523
|
-
}
|
|
3524
|
-
const oldState = this.state;
|
|
3525
|
-
this.state = newState;
|
|
3526
|
-
console.debug(`[ConnectionMonitor] ${oldState} → ${newState}: ${reason}`);
|
|
3527
|
-
this.callbacks.forEach((callback) => {
|
|
3528
|
-
try {
|
|
3529
|
-
callback(newState, reason);
|
|
3530
|
-
} catch (error) {
|
|
3531
|
-
console.error("[ConnectionMonitor] Error in callback:", error);
|
|
3532
|
-
}
|
|
3533
|
-
});
|
|
3534
|
-
}
|
|
3535
|
-
}
|
|
3536
|
-
// src/core/connection/utils.ts
|
|
3537
|
-
function createDisplayAlert(authContext) {
|
|
3538
|
-
return (message, options) => {
|
|
3539
|
-
if (authContext?.isInIframe && typeof globalThis.window !== "undefined" && globalThis.window.parent !== globalThis.window) {
|
|
3540
|
-
window.parent.postMessage({
|
|
3541
|
-
type: "PLAYCADEMY_DISPLAY_ALERT",
|
|
3542
|
-
message,
|
|
3543
|
-
options
|
|
3544
|
-
}, "*");
|
|
3545
|
-
} else {
|
|
3546
|
-
const prefixMap = { error: "❌", warning: "⚠️", info: "ℹ️" };
|
|
3547
|
-
const prefix = (options?.type && prefixMap[options.type]) ?? "ℹ️";
|
|
3548
|
-
console.log(`${prefix} ${message}`);
|
|
3549
|
-
}
|
|
3550
|
-
};
|
|
3551
|
-
}
|
|
3552
|
-
|
|
3553
|
-
// src/core/connection/manager.ts
|
|
3554
|
-
class ConnectionManager {
|
|
3555
|
-
monitor;
|
|
3556
|
-
authContext;
|
|
3557
|
-
disconnectHandler;
|
|
3558
|
-
connectionChangeCallback;
|
|
3559
|
-
currentState = "online";
|
|
3560
|
-
additionalDisconnectHandlers = new Set;
|
|
3561
|
-
constructor(config) {
|
|
3562
|
-
this.authContext = config.authContext;
|
|
3563
|
-
this.disconnectHandler = config.onDisconnect;
|
|
3564
|
-
this.connectionChangeCallback = config.onConnectionChange;
|
|
3565
|
-
if (config.authContext?.isInIframe) {
|
|
3566
|
-
this._setupPlatformListener();
|
|
3567
|
-
}
|
|
3568
|
-
}
|
|
3569
|
-
getState() {
|
|
3570
|
-
return this.monitor?.getState() ?? this.currentState;
|
|
3571
|
-
}
|
|
3572
|
-
async checkNow() {
|
|
3573
|
-
if (!this.monitor) {
|
|
3574
|
-
return this.currentState;
|
|
3575
|
-
}
|
|
3576
|
-
return await this.monitor.checkNow();
|
|
3577
|
-
}
|
|
3578
|
-
reportRequestSuccess() {
|
|
3579
|
-
this.monitor?.reportRequestSuccess();
|
|
3580
|
-
}
|
|
3581
|
-
reportRequestFailure(error) {
|
|
3582
|
-
this.monitor?.reportRequestFailure(error);
|
|
3583
|
-
}
|
|
3584
|
-
onDisconnect(callback) {
|
|
3585
|
-
this.additionalDisconnectHandlers.add(callback);
|
|
3586
|
-
return () => {
|
|
3587
|
-
this.additionalDisconnectHandlers.delete(callback);
|
|
3588
|
-
};
|
|
3589
|
-
}
|
|
3590
|
-
stop() {
|
|
3591
|
-
this.monitor?.stop();
|
|
3592
|
-
}
|
|
3593
|
-
_setupPlatformListener() {
|
|
3594
|
-
messaging.listen("PLAYCADEMY_CONNECTION_STATE" /* CONNECTION_STATE */, ({ state, reason }) => {
|
|
3595
|
-
this.currentState = state;
|
|
3596
|
-
this._handleConnectionChange(state, reason);
|
|
3597
|
-
});
|
|
3598
|
-
}
|
|
3599
|
-
_handleConnectionChange(state, reason) {
|
|
3600
|
-
this.connectionChangeCallback?.(state, reason);
|
|
3601
|
-
if (state === "offline" || state === "degraded") {
|
|
3602
|
-
const context = {
|
|
3603
|
-
state,
|
|
3604
|
-
reason,
|
|
3605
|
-
timestamp: Date.now(),
|
|
3606
|
-
displayAlert: createDisplayAlert(this.authContext)
|
|
3607
|
-
};
|
|
3608
|
-
if (this.disconnectHandler) {
|
|
3609
|
-
this.disconnectHandler(context);
|
|
3610
|
-
}
|
|
3611
|
-
this.additionalDisconnectHandlers.forEach((handler) => {
|
|
3612
|
-
handler(context);
|
|
3613
|
-
});
|
|
3614
|
-
}
|
|
3615
|
-
}
|
|
3616
|
-
}
|
|
3617
2717
|
// src/core/transport/retry.ts
|
|
3618
2718
|
var RETRY_DELAYS_MS = [500, 1500];
|
|
3619
2719
|
function wait(ms) {
|
|
@@ -3763,15 +2863,9 @@ class PlaycademyBaseClient {
|
|
|
3763
2863
|
gameId;
|
|
3764
2864
|
config;
|
|
3765
2865
|
listeners = {};
|
|
3766
|
-
internalClientSessionId;
|
|
3767
2866
|
authContext;
|
|
3768
2867
|
initPayload;
|
|
3769
|
-
connectionManager;
|
|
3770
2868
|
launchId;
|
|
3771
|
-
_sessionManager = {
|
|
3772
|
-
startSession: async (gameId) => this.request(`/games/${gameId}/sessions`, "POST"),
|
|
3773
|
-
endSession: async (sessionId, gameId) => this.request(`/games/${gameId}/sessions/${sessionId}`, "DELETE")
|
|
3774
|
-
};
|
|
3775
2869
|
constructor(config) {
|
|
3776
2870
|
this.baseUrl = config?.baseUrl?.endsWith("/api") ? config.baseUrl : `${config?.baseUrl}/api`;
|
|
3777
2871
|
this.gameUrl = config?.gameUrl;
|
|
@@ -3781,8 +2875,6 @@ class PlaycademyBaseClient {
|
|
|
3781
2875
|
this.config = config || {};
|
|
3782
2876
|
this.authStrategy = createAuthStrategy(config?.token ?? null, config?.tokenType);
|
|
3783
2877
|
this._detectAuthContext();
|
|
3784
|
-
this._initializeInternalSession().catch(() => {});
|
|
3785
|
-
this._initializeConnectionMonitor();
|
|
3786
2878
|
}
|
|
3787
2879
|
getBaseUrl() {
|
|
3788
2880
|
const isRelative = this.baseUrl.startsWith("/");
|
|
@@ -3820,21 +2912,6 @@ class PlaycademyBaseClient {
|
|
|
3820
2912
|
onAuthChange(callback) {
|
|
3821
2913
|
this.on("authChange", (payload) => callback(payload.token));
|
|
3822
2914
|
}
|
|
3823
|
-
onDisconnect(callback) {
|
|
3824
|
-
if (!this.connectionManager) {
|
|
3825
|
-
return () => {};
|
|
3826
|
-
}
|
|
3827
|
-
return this.connectionManager.onDisconnect(callback);
|
|
3828
|
-
}
|
|
3829
|
-
getConnectionState() {
|
|
3830
|
-
return this.connectionManager?.getState() ?? "unknown";
|
|
3831
|
-
}
|
|
3832
|
-
async checkConnection() {
|
|
3833
|
-
if (!this.connectionManager) {
|
|
3834
|
-
return "unknown";
|
|
3835
|
-
}
|
|
3836
|
-
return await this.connectionManager.checkNow();
|
|
3837
|
-
}
|
|
3838
2915
|
_setAuthContext(context) {
|
|
3839
2916
|
this.authContext = context;
|
|
3840
2917
|
}
|
|
@@ -3864,44 +2941,30 @@ class PlaycademyBaseClient {
|
|
|
3864
2941
|
...this.authStrategy.getHeaders(),
|
|
3865
2942
|
...this.launchId ? { "x-playcademy-launch-id": this.launchId } : {}
|
|
3866
2943
|
};
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
});
|
|
3877
|
-
this.connectionManager?.reportRequestSuccess();
|
|
3878
|
-
return result;
|
|
3879
|
-
} catch (error) {
|
|
3880
|
-
this.connectionManager?.reportRequestFailure(error);
|
|
3881
|
-
throw error;
|
|
3882
|
-
}
|
|
2944
|
+
return request({
|
|
2945
|
+
path,
|
|
2946
|
+
method,
|
|
2947
|
+
body: options?.body,
|
|
2948
|
+
baseUrl: this.baseUrl,
|
|
2949
|
+
extraHeaders: effectiveHeaders,
|
|
2950
|
+
raw: options?.raw,
|
|
2951
|
+
retryPolicy: options?.retryPolicy
|
|
2952
|
+
});
|
|
3883
2953
|
}
|
|
3884
2954
|
async requestGameBackend(path, method, body, headers, options) {
|
|
3885
2955
|
const effectiveHeaders = {
|
|
3886
2956
|
...headers,
|
|
3887
2957
|
...this.authStrategy.getHeaders()
|
|
3888
2958
|
};
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
});
|
|
3899
|
-
this.connectionManager?.reportRequestSuccess();
|
|
3900
|
-
return result;
|
|
3901
|
-
} catch (error) {
|
|
3902
|
-
this.connectionManager?.reportRequestFailure(error);
|
|
3903
|
-
throw error;
|
|
3904
|
-
}
|
|
2959
|
+
return request({
|
|
2960
|
+
path,
|
|
2961
|
+
method,
|
|
2962
|
+
body,
|
|
2963
|
+
baseUrl: this.getGameBackendUrl(),
|
|
2964
|
+
extraHeaders: effectiveHeaders,
|
|
2965
|
+
raw: options?.raw,
|
|
2966
|
+
retryPolicy: options?.retryPolicy
|
|
2967
|
+
});
|
|
3905
2968
|
}
|
|
3906
2969
|
_ensureGameId() {
|
|
3907
2970
|
if (!this.gameId) {
|
|
@@ -3912,49 +2975,6 @@ class PlaycademyBaseClient {
|
|
|
3912
2975
|
_detectAuthContext() {
|
|
3913
2976
|
this.authContext = { isInIframe: isInIframe() };
|
|
3914
2977
|
}
|
|
3915
|
-
_initializeConnectionMonitor() {
|
|
3916
|
-
if (typeof globalThis.window === "undefined") {
|
|
3917
|
-
return;
|
|
3918
|
-
}
|
|
3919
|
-
const isEnabled = this.config.enableConnectionMonitoring ?? true;
|
|
3920
|
-
if (!isEnabled) {
|
|
3921
|
-
return;
|
|
3922
|
-
}
|
|
3923
|
-
try {
|
|
3924
|
-
this.connectionManager = new ConnectionManager({
|
|
3925
|
-
baseUrl: this.baseUrl,
|
|
3926
|
-
authContext: this.authContext,
|
|
3927
|
-
onDisconnect: this.config.onDisconnect,
|
|
3928
|
-
onConnectionChange: (state, reason) => {
|
|
3929
|
-
this.emit("connectionChange", { state, reason });
|
|
3930
|
-
}
|
|
3931
|
-
});
|
|
3932
|
-
} catch (error) {
|
|
3933
|
-
log.error("[Playcademy SDK] Failed to initialize connection manager:", { error });
|
|
3934
|
-
}
|
|
3935
|
-
}
|
|
3936
|
-
async _initializeInternalSession() {
|
|
3937
|
-
if (!this.gameId || this.internalClientSessionId) {
|
|
3938
|
-
return;
|
|
3939
|
-
}
|
|
3940
|
-
const shouldAutoStart = this.config.autoStartSession ?? true;
|
|
3941
|
-
if (!shouldAutoStart) {
|
|
3942
|
-
return;
|
|
3943
|
-
}
|
|
3944
|
-
try {
|
|
3945
|
-
const response = await this._sessionManager.startSession(this.gameId);
|
|
3946
|
-
this.internalClientSessionId = response.sessionId;
|
|
3947
|
-
log.debug("[Playcademy SDK] Auto-started game session", {
|
|
3948
|
-
gameId: this.gameId,
|
|
3949
|
-
sessionId: this.internalClientSessionId
|
|
3950
|
-
});
|
|
3951
|
-
} catch (error) {
|
|
3952
|
-
log.error("[Playcademy SDK] Auto-starting session failed for game", {
|
|
3953
|
-
gameId: this.gameId,
|
|
3954
|
-
error
|
|
3955
|
-
});
|
|
3956
|
-
}
|
|
3957
|
-
}
|
|
3958
2978
|
users = createUsersNamespace(this);
|
|
3959
2979
|
}
|
|
3960
2980
|
|
|
@@ -3962,22 +2982,12 @@ class PlaycademyBaseClient {
|
|
|
3962
2982
|
class PlaycademyInternalClient extends PlaycademyBaseClient {
|
|
3963
2983
|
identity = createIdentityNamespace(this);
|
|
3964
2984
|
runtime = createRuntimeNamespace(this);
|
|
3965
|
-
credits = createCreditsNamespace(this);
|
|
3966
|
-
realtime = createRealtimeNamespace(this);
|
|
3967
2985
|
backend = createBackendNamespace(this);
|
|
3968
2986
|
auth = createAuthNamespace(this);
|
|
3969
2987
|
admin = createAdminNamespace(this);
|
|
3970
2988
|
dev = createDevNamespace(this);
|
|
3971
2989
|
games = createGamesNamespace(this);
|
|
3972
|
-
character = createCharacterNamespace(this);
|
|
3973
|
-
achievements = createAchievementsNamespace(this);
|
|
3974
2990
|
leaderboard = createLeaderboardNamespace(this);
|
|
3975
|
-
levels = createLevelsNamespace(this);
|
|
3976
|
-
shop = createShopNamespace(this);
|
|
3977
|
-
notifications = createNotificationsNamespace(this);
|
|
3978
|
-
maps = createMapsNamespace(this);
|
|
3979
|
-
sprites = createSpritesNamespace(this);
|
|
3980
|
-
telemetry = createTelemetryNamespace(this);
|
|
3981
2991
|
scores = createScoresNamespace2(this);
|
|
3982
2992
|
timeback = createTimebackNamespace2(this);
|
|
3983
2993
|
static init = init;
|
|
@@ -3991,7 +3001,5 @@ export {
|
|
|
3991
3001
|
PlaycademyError,
|
|
3992
3002
|
PlaycademyInternalClient as PlaycademyClient,
|
|
3993
3003
|
MessageEvents,
|
|
3994
|
-
ConnectionMonitor,
|
|
3995
|
-
ConnectionManager,
|
|
3996
3004
|
ApiError
|
|
3997
3005
|
};
|