@playcademy/sdk 0.6.1-beta.3 → 0.6.1-beta.4
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/dist/index.d.ts +15 -0
- package/dist/index.js +195 -145
- package/dist/internal.js +195 -145
- package/dist/types.d.ts +15 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -476,6 +476,14 @@ interface EndActivityScoreData {
|
|
|
476
476
|
* @module types/timeback/api
|
|
477
477
|
*/
|
|
478
478
|
|
|
479
|
+
type TimebackPromotionStatus = 'promoted' | 'no-next-course' | 'already-promoted' | 'not-enrolled' | 'not-mastered';
|
|
480
|
+
interface TimebackPromotionResult {
|
|
481
|
+
status: TimebackPromotionStatus;
|
|
482
|
+
currentCourseId: string;
|
|
483
|
+
nextCourseId?: string;
|
|
484
|
+
masteredUnits?: number;
|
|
485
|
+
masterableUnits?: number;
|
|
486
|
+
}
|
|
479
487
|
interface EndActivityResponse {
|
|
480
488
|
status: 'ok';
|
|
481
489
|
courseId: string;
|
|
@@ -485,6 +493,10 @@ interface EndActivityResponse {
|
|
|
485
493
|
scoreStatus?: string;
|
|
486
494
|
inProgress?: string;
|
|
487
495
|
}
|
|
496
|
+
interface AdvanceCourseResponse {
|
|
497
|
+
status: 'ok';
|
|
498
|
+
promotion: TimebackPromotionResult;
|
|
499
|
+
}
|
|
488
500
|
|
|
489
501
|
/**
|
|
490
502
|
* User Types
|
|
@@ -1802,6 +1814,9 @@ declare class PlaycademyClient extends PlaycademyBaseClient {
|
|
|
1802
1814
|
pauseActivity: () => void;
|
|
1803
1815
|
resumeActivity: () => void;
|
|
1804
1816
|
endActivity: (data: EndActivityScoreData) => Promise<EndActivityResponse>;
|
|
1817
|
+
advanceCourse: (options?: {
|
|
1818
|
+
subject?: TimebackSubject | undefined;
|
|
1819
|
+
} | undefined) => Promise<AdvanceCourseResponse>;
|
|
1805
1820
|
};
|
|
1806
1821
|
/**
|
|
1807
1822
|
* Playcademy Credits (platform currency) management.
|
package/dist/index.js
CHANGED
|
@@ -1330,7 +1330,8 @@ var BADGES = {
|
|
|
1330
1330
|
var TIMEBACK_ROUTES = {
|
|
1331
1331
|
END_ACTIVITY: "/integrations/timeback/end-activity",
|
|
1332
1332
|
GET_XP: "/integrations/timeback/xp",
|
|
1333
|
-
HEARTBEAT: "/integrations/timeback/heartbeat"
|
|
1333
|
+
HEARTBEAT: "/integrations/timeback/heartbeat",
|
|
1334
|
+
ADVANCE_COURSE: "/integrations/timeback/advance-course"
|
|
1334
1335
|
};
|
|
1335
1336
|
// src/core/cache/singleton-cache.ts
|
|
1336
1337
|
function createSingletonCache() {
|
|
@@ -1441,6 +1442,99 @@ function createRealtimeNamespace(client) {
|
|
|
1441
1442
|
}
|
|
1442
1443
|
};
|
|
1443
1444
|
}
|
|
1445
|
+
// src/core/guards.ts
|
|
1446
|
+
var VALID_GRADES = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
|
|
1447
|
+
var VALID_SUBJECTS = [
|
|
1448
|
+
"Reading",
|
|
1449
|
+
"Language",
|
|
1450
|
+
"Vocabulary",
|
|
1451
|
+
"Social Studies",
|
|
1452
|
+
"Writing",
|
|
1453
|
+
"Science",
|
|
1454
|
+
"FastMath",
|
|
1455
|
+
"Math",
|
|
1456
|
+
"None"
|
|
1457
|
+
];
|
|
1458
|
+
function isValidGrade(value) {
|
|
1459
|
+
return typeof value === "number" && Number.isInteger(value) && VALID_GRADES.includes(value);
|
|
1460
|
+
}
|
|
1461
|
+
function isValidSubject(value) {
|
|
1462
|
+
return typeof value === "string" && VALID_SUBJECTS.includes(value);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// src/core/cache/ttl-cache.ts
|
|
1466
|
+
function createTTLCache(options) {
|
|
1467
|
+
const cache = new Map;
|
|
1468
|
+
const { ttl: defaultTTL, keyPrefix = "", onClear } = options;
|
|
1469
|
+
async function get(key, loader, config) {
|
|
1470
|
+
const fullKey = keyPrefix ? `${keyPrefix}:${key}` : key;
|
|
1471
|
+
const now = Date.now();
|
|
1472
|
+
const effectiveTTL = config?.ttl !== undefined ? config.ttl : defaultTTL;
|
|
1473
|
+
const force = config?.force || false;
|
|
1474
|
+
const skipCache = config?.skipCache || false;
|
|
1475
|
+
if (effectiveTTL === 0 || skipCache) {
|
|
1476
|
+
return loader();
|
|
1477
|
+
}
|
|
1478
|
+
if (!force) {
|
|
1479
|
+
const cached = cache.get(fullKey);
|
|
1480
|
+
if (cached && cached.expiresAt > now) {
|
|
1481
|
+
return cached.value;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
const promise = loader().catch((error) => {
|
|
1485
|
+
cache.delete(fullKey);
|
|
1486
|
+
throw error;
|
|
1487
|
+
});
|
|
1488
|
+
cache.set(fullKey, {
|
|
1489
|
+
value: promise,
|
|
1490
|
+
expiresAt: now + effectiveTTL
|
|
1491
|
+
});
|
|
1492
|
+
return promise;
|
|
1493
|
+
}
|
|
1494
|
+
function clear(key) {
|
|
1495
|
+
if (key === undefined) {
|
|
1496
|
+
cache.clear();
|
|
1497
|
+
onClear?.();
|
|
1498
|
+
} else {
|
|
1499
|
+
const fullKey = keyPrefix ? `${keyPrefix}:${key}` : key;
|
|
1500
|
+
cache.delete(fullKey);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
function size() {
|
|
1504
|
+
return cache.size;
|
|
1505
|
+
}
|
|
1506
|
+
function prune() {
|
|
1507
|
+
const now = Date.now();
|
|
1508
|
+
for (const [key, entry] of cache.entries()) {
|
|
1509
|
+
if (entry.expiresAt <= now) {
|
|
1510
|
+
cache.delete(key);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
function getKeys() {
|
|
1515
|
+
const keys = [];
|
|
1516
|
+
const prefixLen = keyPrefix ? keyPrefix.length + 1 : 0;
|
|
1517
|
+
for (const fullKey of cache.keys()) {
|
|
1518
|
+
keys.push(fullKey.substring(prefixLen));
|
|
1519
|
+
}
|
|
1520
|
+
return keys;
|
|
1521
|
+
}
|
|
1522
|
+
function has(key) {
|
|
1523
|
+
const fullKey = keyPrefix ? `${keyPrefix}:${key}` : key;
|
|
1524
|
+
const cached = cache.get(fullKey);
|
|
1525
|
+
if (!cached) {
|
|
1526
|
+
return false;
|
|
1527
|
+
}
|
|
1528
|
+
const now = Date.now();
|
|
1529
|
+
if (cached.expiresAt <= now) {
|
|
1530
|
+
cache.delete(fullKey);
|
|
1531
|
+
return false;
|
|
1532
|
+
}
|
|
1533
|
+
return true;
|
|
1534
|
+
}
|
|
1535
|
+
return { get, clear, size, prune, getKeys, has };
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1444
1538
|
// ../utils/src/uuid.ts
|
|
1445
1539
|
var UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
1446
1540
|
function isValidUUID(value) {
|
|
@@ -1450,7 +1544,7 @@ function isValidUUID(value) {
|
|
|
1450
1544
|
return UUID_REGEX.test(value);
|
|
1451
1545
|
}
|
|
1452
1546
|
|
|
1453
|
-
// src/core/activity-tracker.ts
|
|
1547
|
+
// src/core/timeback/activity-tracker.ts
|
|
1454
1548
|
var DEFAULT_PAUSED_HEARTBEAT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
1455
1549
|
var DEFAULT_HEARTBEAT_INTERVAL_MS = 15000;
|
|
1456
1550
|
var DEFAULT_INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
|
|
@@ -1923,102 +2017,29 @@ function createTimebackActivityTracker(client) {
|
|
|
1923
2017
|
};
|
|
1924
2018
|
}
|
|
1925
2019
|
|
|
1926
|
-
// src/core/
|
|
1927
|
-
function
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
return cached.value;
|
|
1943
|
-
}
|
|
1944
|
-
}
|
|
1945
|
-
const promise = loader().catch((error) => {
|
|
1946
|
-
cache.delete(fullKey);
|
|
1947
|
-
throw error;
|
|
1948
|
-
});
|
|
1949
|
-
cache.set(fullKey, {
|
|
1950
|
-
value: promise,
|
|
1951
|
-
expiresAt: now + effectiveTTL
|
|
1952
|
-
});
|
|
1953
|
-
return promise;
|
|
1954
|
-
}
|
|
1955
|
-
function clear(key) {
|
|
1956
|
-
if (key === undefined) {
|
|
1957
|
-
cache.clear();
|
|
1958
|
-
onClear?.();
|
|
1959
|
-
} else {
|
|
1960
|
-
const fullKey = keyPrefix ? `${keyPrefix}:${key}` : key;
|
|
1961
|
-
cache.delete(fullKey);
|
|
1962
|
-
}
|
|
1963
|
-
}
|
|
1964
|
-
function size() {
|
|
1965
|
-
return cache.size;
|
|
1966
|
-
}
|
|
1967
|
-
function prune() {
|
|
1968
|
-
const now = Date.now();
|
|
1969
|
-
for (const [key, entry] of cache.entries()) {
|
|
1970
|
-
if (entry.expiresAt <= now) {
|
|
1971
|
-
cache.delete(key);
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
1974
|
-
}
|
|
1975
|
-
function getKeys() {
|
|
1976
|
-
const keys = [];
|
|
1977
|
-
const prefixLen = keyPrefix ? keyPrefix.length + 1 : 0;
|
|
1978
|
-
for (const fullKey of cache.keys()) {
|
|
1979
|
-
keys.push(fullKey.substring(prefixLen));
|
|
1980
|
-
}
|
|
1981
|
-
return keys;
|
|
1982
|
-
}
|
|
1983
|
-
function has(key) {
|
|
1984
|
-
const fullKey = keyPrefix ? `${keyPrefix}:${key}` : key;
|
|
1985
|
-
const cached = cache.get(fullKey);
|
|
1986
|
-
if (!cached) {
|
|
1987
|
-
return false;
|
|
1988
|
-
}
|
|
1989
|
-
const now = Date.now();
|
|
1990
|
-
if (cached.expiresAt <= now) {
|
|
1991
|
-
cache.delete(fullKey);
|
|
1992
|
-
return false;
|
|
2020
|
+
// src/core/timeback/user.ts
|
|
2021
|
+
function createTimebackUserStore(client) {
|
|
2022
|
+
let override;
|
|
2023
|
+
return {
|
|
2024
|
+
snapshot() {
|
|
2025
|
+
return override ?? client["initPayload"]?.timeback;
|
|
2026
|
+
},
|
|
2027
|
+
async refresh() {
|
|
2028
|
+
const response = await client["request"]("/timeback/user", "GET");
|
|
2029
|
+
override = response;
|
|
2030
|
+
return {
|
|
2031
|
+
id: response.id,
|
|
2032
|
+
role: response.role,
|
|
2033
|
+
enrollments: response.enrollments,
|
|
2034
|
+
organizations: response.organizations
|
|
2035
|
+
};
|
|
1993
2036
|
}
|
|
1994
|
-
|
|
1995
|
-
}
|
|
1996
|
-
return { get, clear, size, prune, getKeys, has };
|
|
1997
|
-
}
|
|
1998
|
-
|
|
1999
|
-
// src/core/guards.ts
|
|
2000
|
-
var VALID_GRADES = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
|
|
2001
|
-
var VALID_SUBJECTS = [
|
|
2002
|
-
"Reading",
|
|
2003
|
-
"Language",
|
|
2004
|
-
"Vocabulary",
|
|
2005
|
-
"Social Studies",
|
|
2006
|
-
"Writing",
|
|
2007
|
-
"Science",
|
|
2008
|
-
"FastMath",
|
|
2009
|
-
"Math",
|
|
2010
|
-
"None"
|
|
2011
|
-
];
|
|
2012
|
-
function isValidGrade(value) {
|
|
2013
|
-
return typeof value === "number" && Number.isInteger(value) && VALID_GRADES.includes(value);
|
|
2014
|
-
}
|
|
2015
|
-
function isValidSubject(value) {
|
|
2016
|
-
return typeof value === "string" && VALID_SUBJECTS.includes(value);
|
|
2037
|
+
};
|
|
2017
2038
|
}
|
|
2018
2039
|
|
|
2019
|
-
// src/
|
|
2020
|
-
function
|
|
2021
|
-
const
|
|
2040
|
+
// src/core/timeback/engine.ts
|
|
2041
|
+
function createTimebackEngine(client) {
|
|
2042
|
+
const userStore = createTimebackUserStore(client);
|
|
2022
2043
|
const userCache = createTTLCache({
|
|
2023
2044
|
ttl: 5 * 60 * 1000,
|
|
2024
2045
|
keyPrefix: "game.timeback.user"
|
|
@@ -2027,42 +2048,84 @@ function createTimebackNamespace(client) {
|
|
|
2027
2048
|
ttl: 5000,
|
|
2028
2049
|
keyPrefix: "game.timeback.xp"
|
|
2029
2050
|
});
|
|
2030
|
-
function
|
|
2031
|
-
|
|
2051
|
+
async function applyPromotion(promotion) {
|
|
2052
|
+
if (promotion.status !== "promoted" && promotion.status !== "already-promoted") {
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
2055
|
+
userCache.clear("current");
|
|
2056
|
+
try {
|
|
2057
|
+
await userStore.refresh();
|
|
2058
|
+
} catch {}
|
|
2032
2059
|
}
|
|
2060
|
+
const activityTracker = createTimebackActivityTracker(client);
|
|
2061
|
+
return {
|
|
2062
|
+
user: {
|
|
2063
|
+
snapshot: () => userStore.snapshot(),
|
|
2064
|
+
fetch: (options) => userCache.get("current", () => userStore.refresh(), options),
|
|
2065
|
+
xp: {
|
|
2066
|
+
fetch: (options) => {
|
|
2067
|
+
const cacheKey = [
|
|
2068
|
+
options.grade ?? "",
|
|
2069
|
+
options.subject ?? "",
|
|
2070
|
+
options.include?.toSorted().join(",") ?? ""
|
|
2071
|
+
].join(":");
|
|
2072
|
+
return xpCache.get(cacheKey, async () => {
|
|
2073
|
+
const params = new URLSearchParams;
|
|
2074
|
+
if (options.grade !== undefined) {
|
|
2075
|
+
params.set("grade", String(options.grade));
|
|
2076
|
+
}
|
|
2077
|
+
if (options.subject !== undefined) {
|
|
2078
|
+
params.set("subject", options.subject);
|
|
2079
|
+
}
|
|
2080
|
+
if (options.include?.length) {
|
|
2081
|
+
params.set("include", options.include.join(","));
|
|
2082
|
+
}
|
|
2083
|
+
const queryString = params.toString();
|
|
2084
|
+
const endpoint = `${TIMEBACK_ROUTES.GET_XP}${queryString ? `?${queryString}` : ""}`;
|
|
2085
|
+
return client["requestGameBackend"](endpoint, "GET");
|
|
2086
|
+
}, { force: options.force });
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
},
|
|
2090
|
+
activity: {
|
|
2091
|
+
start: activityTracker.startActivity,
|
|
2092
|
+
pause: activityTracker.pauseActivity,
|
|
2093
|
+
resume: activityTracker.resumeActivity,
|
|
2094
|
+
end: activityTracker.endActivity
|
|
2095
|
+
},
|
|
2096
|
+
async advanceCourse(params) {
|
|
2097
|
+
const response = await client["requestGameBackend"](TIMEBACK_ROUTES.ADVANCE_COURSE, "POST", params?.subject !== undefined ? { subject: params.subject } : {});
|
|
2098
|
+
await applyPromotion(response.promotion);
|
|
2099
|
+
return response;
|
|
2100
|
+
}
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
// src/namespaces/game/timeback.ts
|
|
2105
|
+
var VALID_INCLUDE_OPTIONS = ["perCourse", "today"];
|
|
2106
|
+
function createTimebackNamespace(client) {
|
|
2107
|
+
const engine = createTimebackEngine(client);
|
|
2033
2108
|
return {
|
|
2034
2109
|
get user() {
|
|
2035
2110
|
assertPlatformMode(client, "timeback.user");
|
|
2036
2111
|
return {
|
|
2037
2112
|
get id() {
|
|
2038
|
-
return
|
|
2113
|
+
return engine.user.snapshot()?.id;
|
|
2039
2114
|
},
|
|
2040
2115
|
get role() {
|
|
2041
|
-
return
|
|
2116
|
+
return engine.user.snapshot()?.role;
|
|
2042
2117
|
},
|
|
2043
2118
|
get enrollments() {
|
|
2044
|
-
return
|
|
2119
|
+
return engine.user.snapshot()?.enrollments ?? [];
|
|
2045
2120
|
},
|
|
2046
2121
|
get organizations() {
|
|
2047
|
-
return
|
|
2122
|
+
return engine.user.snapshot()?.organizations ?? [];
|
|
2048
2123
|
},
|
|
2049
|
-
fetch:
|
|
2050
|
-
const response = await client["request"]("/timeback/user", "GET");
|
|
2051
|
-
const initPayload = client["initPayload"];
|
|
2052
|
-
if (initPayload) {
|
|
2053
|
-
initPayload.timeback = response;
|
|
2054
|
-
}
|
|
2055
|
-
return {
|
|
2056
|
-
id: response.id,
|
|
2057
|
-
role: response.role,
|
|
2058
|
-
enrollments: response.enrollments,
|
|
2059
|
-
organizations: response.organizations
|
|
2060
|
-
};
|
|
2061
|
-
}, options),
|
|
2124
|
+
fetch: (options) => engine.user.fetch(options),
|
|
2062
2125
|
xp: {
|
|
2063
|
-
fetch: async (options) => {
|
|
2064
|
-
const hasGrade = options
|
|
2065
|
-
const hasSubject = options
|
|
2126
|
+
fetch: async (options = {}) => {
|
|
2127
|
+
const hasGrade = options.grade !== undefined;
|
|
2128
|
+
const hasSubject = options.subject !== undefined;
|
|
2066
2129
|
if (hasGrade !== hasSubject) {
|
|
2067
2130
|
throw new Error("Both grade and subject must be provided together");
|
|
2068
2131
|
}
|
|
@@ -2072,53 +2135,40 @@ function createTimebackNamespace(client) {
|
|
|
2072
2135
|
if (hasSubject && !isValidSubject(options.subject)) {
|
|
2073
2136
|
throw new Error(`Invalid subject: ${options.subject}. Valid subjects: ${VALID_SUBJECTS.join(", ")}`);
|
|
2074
2137
|
}
|
|
2075
|
-
|
|
2076
|
-
if (options?.include?.length) {
|
|
2138
|
+
if (options.include?.length) {
|
|
2077
2139
|
for (const opt of options.include) {
|
|
2078
|
-
if (!
|
|
2079
|
-
throw new Error(`Invalid include option: ${opt}. Valid options: ${
|
|
2140
|
+
if (!VALID_INCLUDE_OPTIONS.includes(opt)) {
|
|
2141
|
+
throw new Error(`Invalid include option: ${opt}. Valid options: ${VALID_INCLUDE_OPTIONS.join(", ")}`);
|
|
2080
2142
|
}
|
|
2081
2143
|
}
|
|
2082
2144
|
}
|
|
2083
|
-
|
|
2084
|
-
options?.grade ?? "",
|
|
2085
|
-
options?.subject ?? "",
|
|
2086
|
-
options?.include?.toSorted().join(",") ?? ""
|
|
2087
|
-
].join(":");
|
|
2088
|
-
return xpCache.get(cacheKey, async () => {
|
|
2089
|
-
const params = new URLSearchParams;
|
|
2090
|
-
if (hasGrade) {
|
|
2091
|
-
params.set("grade", String(options.grade));
|
|
2092
|
-
}
|
|
2093
|
-
if (hasSubject) {
|
|
2094
|
-
params.set("subject", options.subject);
|
|
2095
|
-
}
|
|
2096
|
-
if (options?.include?.length) {
|
|
2097
|
-
params.set("include", options.include.join(","));
|
|
2098
|
-
}
|
|
2099
|
-
const queryString = params.toString();
|
|
2100
|
-
const endpoint = `${TIMEBACK_ROUTES.GET_XP}${queryString ? `?${queryString}` : ""}`;
|
|
2101
|
-
return client["requestGameBackend"](endpoint, "GET");
|
|
2102
|
-
}, { force: options?.force });
|
|
2145
|
+
return engine.user.xp.fetch(options);
|
|
2103
2146
|
}
|
|
2104
2147
|
}
|
|
2105
2148
|
};
|
|
2106
2149
|
},
|
|
2107
2150
|
startActivity: (metadata, options) => {
|
|
2108
2151
|
assertPlatformMode(client, "timeback.startActivity()");
|
|
2109
|
-
|
|
2152
|
+
engine.activity.start(metadata, options);
|
|
2110
2153
|
},
|
|
2111
2154
|
pauseActivity: () => {
|
|
2112
2155
|
assertPlatformMode(client, "timeback.pauseActivity()");
|
|
2113
|
-
|
|
2156
|
+
engine.activity.pause();
|
|
2114
2157
|
},
|
|
2115
2158
|
resumeActivity: () => {
|
|
2116
2159
|
assertPlatformMode(client, "timeback.resumeActivity()");
|
|
2117
|
-
|
|
2160
|
+
engine.activity.resume();
|
|
2118
2161
|
},
|
|
2119
2162
|
endActivity: async (data) => {
|
|
2120
2163
|
assertPlatformMode(client, "timeback.endActivity()");
|
|
2121
|
-
return
|
|
2164
|
+
return engine.activity.end(data);
|
|
2165
|
+
},
|
|
2166
|
+
advanceCourse: async (options) => {
|
|
2167
|
+
assertPlatformMode(client, "timeback.advanceCourse()");
|
|
2168
|
+
if (options?.subject !== undefined && !isValidSubject(options.subject)) {
|
|
2169
|
+
throw new Error(`Invalid subject: ${options.subject}. Valid subjects: ${VALID_SUBJECTS.join(", ")}`);
|
|
2170
|
+
}
|
|
2171
|
+
return engine.advanceCourse(options);
|
|
2122
2172
|
}
|
|
2123
2173
|
};
|
|
2124
2174
|
}
|
package/dist/internal.js
CHANGED
|
@@ -1330,7 +1330,8 @@ var BADGES = {
|
|
|
1330
1330
|
var TIMEBACK_ROUTES = {
|
|
1331
1331
|
END_ACTIVITY: "/integrations/timeback/end-activity",
|
|
1332
1332
|
GET_XP: "/integrations/timeback/xp",
|
|
1333
|
-
HEARTBEAT: "/integrations/timeback/heartbeat"
|
|
1333
|
+
HEARTBEAT: "/integrations/timeback/heartbeat",
|
|
1334
|
+
ADVANCE_COURSE: "/integrations/timeback/advance-course"
|
|
1334
1335
|
};
|
|
1335
1336
|
// src/core/cache/singleton-cache.ts
|
|
1336
1337
|
function createSingletonCache() {
|
|
@@ -1441,6 +1442,99 @@ function createRealtimeNamespace(client) {
|
|
|
1441
1442
|
}
|
|
1442
1443
|
};
|
|
1443
1444
|
}
|
|
1445
|
+
// src/core/guards.ts
|
|
1446
|
+
var VALID_GRADES = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
|
|
1447
|
+
var VALID_SUBJECTS = [
|
|
1448
|
+
"Reading",
|
|
1449
|
+
"Language",
|
|
1450
|
+
"Vocabulary",
|
|
1451
|
+
"Social Studies",
|
|
1452
|
+
"Writing",
|
|
1453
|
+
"Science",
|
|
1454
|
+
"FastMath",
|
|
1455
|
+
"Math",
|
|
1456
|
+
"None"
|
|
1457
|
+
];
|
|
1458
|
+
function isValidGrade(value) {
|
|
1459
|
+
return typeof value === "number" && Number.isInteger(value) && VALID_GRADES.includes(value);
|
|
1460
|
+
}
|
|
1461
|
+
function isValidSubject(value) {
|
|
1462
|
+
return typeof value === "string" && VALID_SUBJECTS.includes(value);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// src/core/cache/ttl-cache.ts
|
|
1466
|
+
function createTTLCache(options) {
|
|
1467
|
+
const cache = new Map;
|
|
1468
|
+
const { ttl: defaultTTL, keyPrefix = "", onClear } = options;
|
|
1469
|
+
async function get(key, loader, config) {
|
|
1470
|
+
const fullKey = keyPrefix ? `${keyPrefix}:${key}` : key;
|
|
1471
|
+
const now = Date.now();
|
|
1472
|
+
const effectiveTTL = config?.ttl !== undefined ? config.ttl : defaultTTL;
|
|
1473
|
+
const force = config?.force || false;
|
|
1474
|
+
const skipCache = config?.skipCache || false;
|
|
1475
|
+
if (effectiveTTL === 0 || skipCache) {
|
|
1476
|
+
return loader();
|
|
1477
|
+
}
|
|
1478
|
+
if (!force) {
|
|
1479
|
+
const cached = cache.get(fullKey);
|
|
1480
|
+
if (cached && cached.expiresAt > now) {
|
|
1481
|
+
return cached.value;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
const promise = loader().catch((error) => {
|
|
1485
|
+
cache.delete(fullKey);
|
|
1486
|
+
throw error;
|
|
1487
|
+
});
|
|
1488
|
+
cache.set(fullKey, {
|
|
1489
|
+
value: promise,
|
|
1490
|
+
expiresAt: now + effectiveTTL
|
|
1491
|
+
});
|
|
1492
|
+
return promise;
|
|
1493
|
+
}
|
|
1494
|
+
function clear(key) {
|
|
1495
|
+
if (key === undefined) {
|
|
1496
|
+
cache.clear();
|
|
1497
|
+
onClear?.();
|
|
1498
|
+
} else {
|
|
1499
|
+
const fullKey = keyPrefix ? `${keyPrefix}:${key}` : key;
|
|
1500
|
+
cache.delete(fullKey);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
function size() {
|
|
1504
|
+
return cache.size;
|
|
1505
|
+
}
|
|
1506
|
+
function prune() {
|
|
1507
|
+
const now = Date.now();
|
|
1508
|
+
for (const [key, entry] of cache.entries()) {
|
|
1509
|
+
if (entry.expiresAt <= now) {
|
|
1510
|
+
cache.delete(key);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
function getKeys() {
|
|
1515
|
+
const keys = [];
|
|
1516
|
+
const prefixLen = keyPrefix ? keyPrefix.length + 1 : 0;
|
|
1517
|
+
for (const fullKey of cache.keys()) {
|
|
1518
|
+
keys.push(fullKey.substring(prefixLen));
|
|
1519
|
+
}
|
|
1520
|
+
return keys;
|
|
1521
|
+
}
|
|
1522
|
+
function has(key) {
|
|
1523
|
+
const fullKey = keyPrefix ? `${keyPrefix}:${key}` : key;
|
|
1524
|
+
const cached = cache.get(fullKey);
|
|
1525
|
+
if (!cached) {
|
|
1526
|
+
return false;
|
|
1527
|
+
}
|
|
1528
|
+
const now = Date.now();
|
|
1529
|
+
if (cached.expiresAt <= now) {
|
|
1530
|
+
cache.delete(fullKey);
|
|
1531
|
+
return false;
|
|
1532
|
+
}
|
|
1533
|
+
return true;
|
|
1534
|
+
}
|
|
1535
|
+
return { get, clear, size, prune, getKeys, has };
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1444
1538
|
// ../utils/src/uuid.ts
|
|
1445
1539
|
var UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
1446
1540
|
function isValidUUID(value) {
|
|
@@ -1450,7 +1544,7 @@ function isValidUUID(value) {
|
|
|
1450
1544
|
return UUID_REGEX.test(value);
|
|
1451
1545
|
}
|
|
1452
1546
|
|
|
1453
|
-
// src/core/activity-tracker.ts
|
|
1547
|
+
// src/core/timeback/activity-tracker.ts
|
|
1454
1548
|
var DEFAULT_PAUSED_HEARTBEAT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
1455
1549
|
var DEFAULT_HEARTBEAT_INTERVAL_MS = 15000;
|
|
1456
1550
|
var DEFAULT_INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
|
|
@@ -1923,102 +2017,29 @@ function createTimebackActivityTracker(client) {
|
|
|
1923
2017
|
};
|
|
1924
2018
|
}
|
|
1925
2019
|
|
|
1926
|
-
// src/core/
|
|
1927
|
-
function
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
return cached.value;
|
|
1943
|
-
}
|
|
1944
|
-
}
|
|
1945
|
-
const promise = loader().catch((error) => {
|
|
1946
|
-
cache.delete(fullKey);
|
|
1947
|
-
throw error;
|
|
1948
|
-
});
|
|
1949
|
-
cache.set(fullKey, {
|
|
1950
|
-
value: promise,
|
|
1951
|
-
expiresAt: now + effectiveTTL
|
|
1952
|
-
});
|
|
1953
|
-
return promise;
|
|
1954
|
-
}
|
|
1955
|
-
function clear(key) {
|
|
1956
|
-
if (key === undefined) {
|
|
1957
|
-
cache.clear();
|
|
1958
|
-
onClear?.();
|
|
1959
|
-
} else {
|
|
1960
|
-
const fullKey = keyPrefix ? `${keyPrefix}:${key}` : key;
|
|
1961
|
-
cache.delete(fullKey);
|
|
1962
|
-
}
|
|
1963
|
-
}
|
|
1964
|
-
function size() {
|
|
1965
|
-
return cache.size;
|
|
1966
|
-
}
|
|
1967
|
-
function prune() {
|
|
1968
|
-
const now = Date.now();
|
|
1969
|
-
for (const [key, entry] of cache.entries()) {
|
|
1970
|
-
if (entry.expiresAt <= now) {
|
|
1971
|
-
cache.delete(key);
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
1974
|
-
}
|
|
1975
|
-
function getKeys() {
|
|
1976
|
-
const keys = [];
|
|
1977
|
-
const prefixLen = keyPrefix ? keyPrefix.length + 1 : 0;
|
|
1978
|
-
for (const fullKey of cache.keys()) {
|
|
1979
|
-
keys.push(fullKey.substring(prefixLen));
|
|
1980
|
-
}
|
|
1981
|
-
return keys;
|
|
1982
|
-
}
|
|
1983
|
-
function has(key) {
|
|
1984
|
-
const fullKey = keyPrefix ? `${keyPrefix}:${key}` : key;
|
|
1985
|
-
const cached = cache.get(fullKey);
|
|
1986
|
-
if (!cached) {
|
|
1987
|
-
return false;
|
|
1988
|
-
}
|
|
1989
|
-
const now = Date.now();
|
|
1990
|
-
if (cached.expiresAt <= now) {
|
|
1991
|
-
cache.delete(fullKey);
|
|
1992
|
-
return false;
|
|
2020
|
+
// src/core/timeback/user.ts
|
|
2021
|
+
function createTimebackUserStore(client) {
|
|
2022
|
+
let override;
|
|
2023
|
+
return {
|
|
2024
|
+
snapshot() {
|
|
2025
|
+
return override ?? client["initPayload"]?.timeback;
|
|
2026
|
+
},
|
|
2027
|
+
async refresh() {
|
|
2028
|
+
const response = await client["request"]("/timeback/user", "GET");
|
|
2029
|
+
override = response;
|
|
2030
|
+
return {
|
|
2031
|
+
id: response.id,
|
|
2032
|
+
role: response.role,
|
|
2033
|
+
enrollments: response.enrollments,
|
|
2034
|
+
organizations: response.organizations
|
|
2035
|
+
};
|
|
1993
2036
|
}
|
|
1994
|
-
|
|
1995
|
-
}
|
|
1996
|
-
return { get, clear, size, prune, getKeys, has };
|
|
1997
|
-
}
|
|
1998
|
-
|
|
1999
|
-
// src/core/guards.ts
|
|
2000
|
-
var VALID_GRADES = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
|
|
2001
|
-
var VALID_SUBJECTS = [
|
|
2002
|
-
"Reading",
|
|
2003
|
-
"Language",
|
|
2004
|
-
"Vocabulary",
|
|
2005
|
-
"Social Studies",
|
|
2006
|
-
"Writing",
|
|
2007
|
-
"Science",
|
|
2008
|
-
"FastMath",
|
|
2009
|
-
"Math",
|
|
2010
|
-
"None"
|
|
2011
|
-
];
|
|
2012
|
-
function isValidGrade(value) {
|
|
2013
|
-
return typeof value === "number" && Number.isInteger(value) && VALID_GRADES.includes(value);
|
|
2014
|
-
}
|
|
2015
|
-
function isValidSubject(value) {
|
|
2016
|
-
return typeof value === "string" && VALID_SUBJECTS.includes(value);
|
|
2037
|
+
};
|
|
2017
2038
|
}
|
|
2018
2039
|
|
|
2019
|
-
// src/
|
|
2020
|
-
function
|
|
2021
|
-
const
|
|
2040
|
+
// src/core/timeback/engine.ts
|
|
2041
|
+
function createTimebackEngine(client) {
|
|
2042
|
+
const userStore = createTimebackUserStore(client);
|
|
2022
2043
|
const userCache = createTTLCache({
|
|
2023
2044
|
ttl: 5 * 60 * 1000,
|
|
2024
2045
|
keyPrefix: "game.timeback.user"
|
|
@@ -2027,42 +2048,84 @@ function createTimebackNamespace(client) {
|
|
|
2027
2048
|
ttl: 5000,
|
|
2028
2049
|
keyPrefix: "game.timeback.xp"
|
|
2029
2050
|
});
|
|
2030
|
-
function
|
|
2031
|
-
|
|
2051
|
+
async function applyPromotion(promotion) {
|
|
2052
|
+
if (promotion.status !== "promoted" && promotion.status !== "already-promoted") {
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
2055
|
+
userCache.clear("current");
|
|
2056
|
+
try {
|
|
2057
|
+
await userStore.refresh();
|
|
2058
|
+
} catch {}
|
|
2032
2059
|
}
|
|
2060
|
+
const activityTracker = createTimebackActivityTracker(client);
|
|
2061
|
+
return {
|
|
2062
|
+
user: {
|
|
2063
|
+
snapshot: () => userStore.snapshot(),
|
|
2064
|
+
fetch: (options) => userCache.get("current", () => userStore.refresh(), options),
|
|
2065
|
+
xp: {
|
|
2066
|
+
fetch: (options) => {
|
|
2067
|
+
const cacheKey = [
|
|
2068
|
+
options.grade ?? "",
|
|
2069
|
+
options.subject ?? "",
|
|
2070
|
+
options.include?.toSorted().join(",") ?? ""
|
|
2071
|
+
].join(":");
|
|
2072
|
+
return xpCache.get(cacheKey, async () => {
|
|
2073
|
+
const params = new URLSearchParams;
|
|
2074
|
+
if (options.grade !== undefined) {
|
|
2075
|
+
params.set("grade", String(options.grade));
|
|
2076
|
+
}
|
|
2077
|
+
if (options.subject !== undefined) {
|
|
2078
|
+
params.set("subject", options.subject);
|
|
2079
|
+
}
|
|
2080
|
+
if (options.include?.length) {
|
|
2081
|
+
params.set("include", options.include.join(","));
|
|
2082
|
+
}
|
|
2083
|
+
const queryString = params.toString();
|
|
2084
|
+
const endpoint = `${TIMEBACK_ROUTES.GET_XP}${queryString ? `?${queryString}` : ""}`;
|
|
2085
|
+
return client["requestGameBackend"](endpoint, "GET");
|
|
2086
|
+
}, { force: options.force });
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
},
|
|
2090
|
+
activity: {
|
|
2091
|
+
start: activityTracker.startActivity,
|
|
2092
|
+
pause: activityTracker.pauseActivity,
|
|
2093
|
+
resume: activityTracker.resumeActivity,
|
|
2094
|
+
end: activityTracker.endActivity
|
|
2095
|
+
},
|
|
2096
|
+
async advanceCourse(params) {
|
|
2097
|
+
const response = await client["requestGameBackend"](TIMEBACK_ROUTES.ADVANCE_COURSE, "POST", params?.subject !== undefined ? { subject: params.subject } : {});
|
|
2098
|
+
await applyPromotion(response.promotion);
|
|
2099
|
+
return response;
|
|
2100
|
+
}
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
// src/namespaces/game/timeback.ts
|
|
2105
|
+
var VALID_INCLUDE_OPTIONS = ["perCourse", "today"];
|
|
2106
|
+
function createTimebackNamespace(client) {
|
|
2107
|
+
const engine = createTimebackEngine(client);
|
|
2033
2108
|
return {
|
|
2034
2109
|
get user() {
|
|
2035
2110
|
assertPlatformMode(client, "timeback.user");
|
|
2036
2111
|
return {
|
|
2037
2112
|
get id() {
|
|
2038
|
-
return
|
|
2113
|
+
return engine.user.snapshot()?.id;
|
|
2039
2114
|
},
|
|
2040
2115
|
get role() {
|
|
2041
|
-
return
|
|
2116
|
+
return engine.user.snapshot()?.role;
|
|
2042
2117
|
},
|
|
2043
2118
|
get enrollments() {
|
|
2044
|
-
return
|
|
2119
|
+
return engine.user.snapshot()?.enrollments ?? [];
|
|
2045
2120
|
},
|
|
2046
2121
|
get organizations() {
|
|
2047
|
-
return
|
|
2122
|
+
return engine.user.snapshot()?.organizations ?? [];
|
|
2048
2123
|
},
|
|
2049
|
-
fetch:
|
|
2050
|
-
const response = await client["request"]("/timeback/user", "GET");
|
|
2051
|
-
const initPayload = client["initPayload"];
|
|
2052
|
-
if (initPayload) {
|
|
2053
|
-
initPayload.timeback = response;
|
|
2054
|
-
}
|
|
2055
|
-
return {
|
|
2056
|
-
id: response.id,
|
|
2057
|
-
role: response.role,
|
|
2058
|
-
enrollments: response.enrollments,
|
|
2059
|
-
organizations: response.organizations
|
|
2060
|
-
};
|
|
2061
|
-
}, options),
|
|
2124
|
+
fetch: (options) => engine.user.fetch(options),
|
|
2062
2125
|
xp: {
|
|
2063
|
-
fetch: async (options) => {
|
|
2064
|
-
const hasGrade = options
|
|
2065
|
-
const hasSubject = options
|
|
2126
|
+
fetch: async (options = {}) => {
|
|
2127
|
+
const hasGrade = options.grade !== undefined;
|
|
2128
|
+
const hasSubject = options.subject !== undefined;
|
|
2066
2129
|
if (hasGrade !== hasSubject) {
|
|
2067
2130
|
throw new Error("Both grade and subject must be provided together");
|
|
2068
2131
|
}
|
|
@@ -2072,53 +2135,40 @@ function createTimebackNamespace(client) {
|
|
|
2072
2135
|
if (hasSubject && !isValidSubject(options.subject)) {
|
|
2073
2136
|
throw new Error(`Invalid subject: ${options.subject}. Valid subjects: ${VALID_SUBJECTS.join(", ")}`);
|
|
2074
2137
|
}
|
|
2075
|
-
|
|
2076
|
-
if (options?.include?.length) {
|
|
2138
|
+
if (options.include?.length) {
|
|
2077
2139
|
for (const opt of options.include) {
|
|
2078
|
-
if (!
|
|
2079
|
-
throw new Error(`Invalid include option: ${opt}. Valid options: ${
|
|
2140
|
+
if (!VALID_INCLUDE_OPTIONS.includes(opt)) {
|
|
2141
|
+
throw new Error(`Invalid include option: ${opt}. Valid options: ${VALID_INCLUDE_OPTIONS.join(", ")}`);
|
|
2080
2142
|
}
|
|
2081
2143
|
}
|
|
2082
2144
|
}
|
|
2083
|
-
|
|
2084
|
-
options?.grade ?? "",
|
|
2085
|
-
options?.subject ?? "",
|
|
2086
|
-
options?.include?.toSorted().join(",") ?? ""
|
|
2087
|
-
].join(":");
|
|
2088
|
-
return xpCache.get(cacheKey, async () => {
|
|
2089
|
-
const params = new URLSearchParams;
|
|
2090
|
-
if (hasGrade) {
|
|
2091
|
-
params.set("grade", String(options.grade));
|
|
2092
|
-
}
|
|
2093
|
-
if (hasSubject) {
|
|
2094
|
-
params.set("subject", options.subject);
|
|
2095
|
-
}
|
|
2096
|
-
if (options?.include?.length) {
|
|
2097
|
-
params.set("include", options.include.join(","));
|
|
2098
|
-
}
|
|
2099
|
-
const queryString = params.toString();
|
|
2100
|
-
const endpoint = `${TIMEBACK_ROUTES.GET_XP}${queryString ? `?${queryString}` : ""}`;
|
|
2101
|
-
return client["requestGameBackend"](endpoint, "GET");
|
|
2102
|
-
}, { force: options?.force });
|
|
2145
|
+
return engine.user.xp.fetch(options);
|
|
2103
2146
|
}
|
|
2104
2147
|
}
|
|
2105
2148
|
};
|
|
2106
2149
|
},
|
|
2107
2150
|
startActivity: (metadata, options) => {
|
|
2108
2151
|
assertPlatformMode(client, "timeback.startActivity()");
|
|
2109
|
-
|
|
2152
|
+
engine.activity.start(metadata, options);
|
|
2110
2153
|
},
|
|
2111
2154
|
pauseActivity: () => {
|
|
2112
2155
|
assertPlatformMode(client, "timeback.pauseActivity()");
|
|
2113
|
-
|
|
2156
|
+
engine.activity.pause();
|
|
2114
2157
|
},
|
|
2115
2158
|
resumeActivity: () => {
|
|
2116
2159
|
assertPlatformMode(client, "timeback.resumeActivity()");
|
|
2117
|
-
|
|
2160
|
+
engine.activity.resume();
|
|
2118
2161
|
},
|
|
2119
2162
|
endActivity: async (data) => {
|
|
2120
2163
|
assertPlatformMode(client, "timeback.endActivity()");
|
|
2121
|
-
return
|
|
2164
|
+
return engine.activity.end(data);
|
|
2165
|
+
},
|
|
2166
|
+
advanceCourse: async (options) => {
|
|
2167
|
+
assertPlatformMode(client, "timeback.advanceCourse()");
|
|
2168
|
+
if (options?.subject !== undefined && !isValidSubject(options.subject)) {
|
|
2169
|
+
throw new Error(`Invalid subject: ${options.subject}. Valid subjects: ${VALID_SUBJECTS.join(", ")}`);
|
|
2170
|
+
}
|
|
2171
|
+
return engine.advanceCourse(options);
|
|
2122
2172
|
}
|
|
2123
2173
|
};
|
|
2124
2174
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -332,6 +332,14 @@ interface GameTimebackIntegration {
|
|
|
332
332
|
createdAt: Date;
|
|
333
333
|
updatedAt: Date;
|
|
334
334
|
}
|
|
335
|
+
type TimebackPromotionStatus = 'promoted' | 'no-next-course' | 'already-promoted' | 'not-enrolled' | 'not-mastered';
|
|
336
|
+
interface TimebackPromotionResult {
|
|
337
|
+
status: TimebackPromotionStatus;
|
|
338
|
+
currentCourseId: string;
|
|
339
|
+
nextCourseId?: string;
|
|
340
|
+
masteredUnits?: number;
|
|
341
|
+
masterableUnits?: number;
|
|
342
|
+
}
|
|
335
343
|
interface EndActivityResponse {
|
|
336
344
|
status: 'ok';
|
|
337
345
|
courseId: string;
|
|
@@ -341,6 +349,10 @@ interface EndActivityResponse {
|
|
|
341
349
|
scoreStatus?: string;
|
|
342
350
|
inProgress?: string;
|
|
343
351
|
}
|
|
352
|
+
interface AdvanceCourseResponse {
|
|
353
|
+
status: 'ok';
|
|
354
|
+
promotion: TimebackPromotionResult;
|
|
355
|
+
}
|
|
344
356
|
|
|
345
357
|
/**
|
|
346
358
|
* Achievement Types
|
|
@@ -5295,6 +5307,9 @@ declare class PlaycademyClient extends PlaycademyBaseClient {
|
|
|
5295
5307
|
pauseActivity: () => void;
|
|
5296
5308
|
resumeActivity: () => void;
|
|
5297
5309
|
endActivity: (data: EndActivityScoreData) => Promise<EndActivityResponse>;
|
|
5310
|
+
advanceCourse: (options?: {
|
|
5311
|
+
subject?: TimebackSubject | undefined;
|
|
5312
|
+
} | undefined) => Promise<AdvanceCourseResponse>;
|
|
5298
5313
|
};
|
|
5299
5314
|
/**
|
|
5300
5315
|
* Playcademy Credits (platform currency) management.
|