@playcademy/sdk 0.6.1-beta.3 → 0.6.1-beta.5

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 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
@@ -775,7 +787,7 @@ declare const items: drizzle_orm_pg_core.PgTableWithColumns<{
775
787
  generated: undefined;
776
788
  }, {}, {}>;
777
789
  };
778
- dialect: "pg";
790
+ dialect: 'pg';
779
791
  }>;
780
792
  declare const inventoryItems: drizzle_orm_pg_core.PgTableWithColumns<{
781
793
  name: "inventory_items";
@@ -867,7 +879,7 @@ declare const inventoryItems: drizzle_orm_pg_core.PgTableWithColumns<{
867
879
  generated: undefined;
868
880
  }, {}, {}>;
869
881
  };
870
- dialect: "pg";
882
+ dialect: 'pg';
871
883
  }>;
872
884
 
873
885
  type ItemRow = typeof items.$inferSelect;
@@ -1748,8 +1760,8 @@ declare class PlaycademyClient extends PlaycademyBaseClient {
1748
1760
  */
1749
1761
  runtime: {
1750
1762
  getGameToken: (gameId: string, options?: {
1751
- apply?: boolean | undefined;
1752
- } | undefined) => Promise<GameTokenResponse>;
1763
+ apply?: boolean;
1764
+ }) => Promise<GameTokenResponse>;
1753
1765
  exit: () => Promise<void>;
1754
1766
  onInit: (handler: (context: GameContextPayload) => void) => void;
1755
1767
  onTokenRefresh: (handler: (data: {
@@ -1773,7 +1785,7 @@ declare class PlaycademyClient extends PlaycademyBaseClient {
1773
1785
  getListenerCounts: () => Record<string, number>;
1774
1786
  assets: {
1775
1787
  url(pathOrStrings: string | TemplateStringsArray, ...values: unknown[]): string;
1776
- fetch: (path: string, options?: RequestInit | undefined) => Promise<Response>;
1788
+ fetch: (path: string, options?: RequestInit) => Promise<Response>;
1777
1789
  json: <T = unknown>(path: string) => Promise<T>;
1778
1790
  blob: (path: string) => Promise<Blob>;
1779
1791
  text: (path: string) => Promise<string>;
@@ -1798,10 +1810,13 @@ declare class PlaycademyClient extends PlaycademyBaseClient {
1798
1810
  */
1799
1811
  timeback: {
1800
1812
  readonly user: TimebackUser;
1801
- startActivity: (metadata: ActivityData, options?: StartActivityOptions | undefined) => void;
1813
+ startActivity: (metadata: ActivityData, options?: StartActivityOptions) => void;
1802
1814
  pauseActivity: () => void;
1803
1815
  resumeActivity: () => void;
1804
1816
  endActivity: (data: EndActivityScoreData) => Promise<EndActivityResponse>;
1817
+ advanceCourse: (options?: {
1818
+ subject?: TimebackSubject;
1819
+ }) => Promise<AdvanceCourseResponse>;
1805
1820
  };
1806
1821
  /**
1807
1822
  * Playcademy Credits (platform currency) management.
@@ -1818,14 +1833,14 @@ declare class PlaycademyClient extends PlaycademyBaseClient {
1818
1833
  * - `submit(score, metadata?)` - Record a game score
1819
1834
  */
1820
1835
  scores: {
1821
- submit: (score: number, metadata?: Record<string, unknown> | undefined) => Promise<ScoreSubmission>;
1836
+ submit: (score: number, metadata?: Record<string, unknown>) => Promise<ScoreSubmission>;
1822
1837
  };
1823
1838
  /**
1824
1839
  * Read-only leaderboard access for the current game scope.
1825
1840
  * - `fetch(options?)` - Fetch leaderboard entries
1826
1841
  */
1827
1842
  leaderboard: {
1828
- fetch: (options?: LeaderboardOptions | undefined) => Promise<GameLeaderboardEntry[]>;
1843
+ fetch: (options?: LeaderboardOptions) => Promise<GameLeaderboardEntry[]>;
1829
1844
  };
1830
1845
  /**
1831
1846
  * Demo-mode helpers. Methods throw when called outside `client.mode === 'demo'`,
@@ -1839,7 +1854,7 @@ declare class PlaycademyClient extends PlaycademyBaseClient {
1839
1854
  get: () => Promise<DemoProfile>;
1840
1855
  update: (updates: DemoProfileUpdate) => Promise<DemoProfile>;
1841
1856
  };
1842
- end: (score: number, options?: DemoEndOptions | undefined) => void;
1857
+ end: (score: number, options?: DemoEndOptions) => void;
1843
1858
  };
1844
1859
  /**
1845
1860
  * Realtime multiplayer authentication.
@@ -1856,13 +1871,13 @@ declare class PlaycademyClient extends PlaycademyBaseClient {
1856
1871
  * - Routes are relative to your game's deployment (e.g., '/hello' → your-game.playcademy.gg/api/hello)
1857
1872
  */
1858
1873
  backend: {
1859
- get<T = unknown>(path: string, headers?: Record<string, string> | undefined): Promise<T>;
1860
- post<T = unknown>(path: string, body?: unknown, headers?: Record<string, string> | undefined): Promise<T>;
1861
- put<T = unknown>(path: string, body?: unknown, headers?: Record<string, string> | undefined): Promise<T>;
1862
- patch<T = unknown>(path: string, body?: unknown, headers?: Record<string, string> | undefined): Promise<T>;
1863
- delete<T = unknown>(path: string, headers?: Record<string, string> | undefined): Promise<T>;
1864
- request<T = unknown>(path: string, method: Method, body?: unknown, headers?: Record<string, string> | undefined): Promise<T>;
1865
- download(path: string, method?: Method, body?: unknown, headers?: Record<string, string> | undefined): Promise<Response>;
1874
+ get<T = unknown>(path: string, headers?: Record<string, string>): Promise<T>;
1875
+ post<T = unknown>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T>;
1876
+ put<T = unknown>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T>;
1877
+ patch<T = unknown>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T>;
1878
+ delete<T = unknown>(path: string, headers?: Record<string, string>): Promise<T>;
1879
+ request<T = unknown>(path: string, method: Method, body?: unknown, headers?: Record<string, string>): Promise<T>;
1880
+ download(path: string, method?: Method, body?: unknown, headers?: Record<string, string>): Promise<Response>;
1866
1881
  url(pathOrStrings: string | TemplateStringsArray, ...values: unknown[]): string;
1867
1882
  };
1868
1883
  /** Auto-initializes a PlaycademyClient with context from the environment */
package/dist/index.js CHANGED
@@ -321,7 +321,7 @@ function formatJSONSingleLine(level, message, context, scope) {
321
321
  ...context && Object.keys(context).length > 0 && { context }
322
322
  };
323
323
  const consoleMethod = getConsoleMethod(level);
324
- consoleMethod(JSON.stringify(logEntry));
324
+ consoleMethod(safeJSONStringify(logEntry));
325
325
  }
326
326
  function formatJSONPretty(level, message, context, scope) {
327
327
  const timestamp = new Date().toISOString();
@@ -333,7 +333,30 @@ function formatJSONPretty(level, message, context, scope) {
333
333
  ...context && Object.keys(context).length > 0 && { context }
334
334
  };
335
335
  const consoleMethod = getConsoleMethod(level);
336
- consoleMethod(JSON.stringify(logEntry, null, 2));
336
+ consoleMethod(safeJSONStringify(logEntry, 2));
337
+ }
338
+ function safeJSONStringify(value, space) {
339
+ const seen = new WeakSet;
340
+ try {
341
+ return JSON.stringify(value, (_key, currentValue) => {
342
+ if (typeof currentValue === "bigint") {
343
+ return currentValue.toString();
344
+ }
345
+ if (typeof currentValue === "object" && currentValue !== null) {
346
+ if (seen.has(currentValue)) {
347
+ return "[Circular]";
348
+ }
349
+ seen.add(currentValue);
350
+ }
351
+ return currentValue;
352
+ }, space);
353
+ } catch {
354
+ return JSON.stringify({
355
+ timestamp: new Date().toISOString(),
356
+ level: "ERROR",
357
+ message: "[Logger] Failed to serialize log entry"
358
+ });
359
+ }
337
360
  }
338
361
  function getConsoleMethod(level) {
339
362
  switch (level) {
@@ -1005,7 +1028,7 @@ function createAssetsNamespace(client) {
1005
1028
  fetch: fetchAsset,
1006
1029
  json: async (path) => {
1007
1030
  const response = await fetchAsset(path);
1008
- return response.json();
1031
+ return await response.json();
1009
1032
  },
1010
1033
  blob: async (path) => {
1011
1034
  const response = await fetchAsset(path);
@@ -1297,10 +1320,10 @@ var ACHIEVEMENT_DEFINITIONS = [
1297
1320
  var TypeScriptPackages = {
1298
1321
  tsc: "tsc",
1299
1322
  nativePreview: "@typescript/native-preview",
1300
- nativePreviewPinned: "@typescript/native-preview@7.0.0-dev.20260221.1"
1323
+ nativePreviewBeta: "@typescript/native-preview@beta"
1301
1324
  };
1302
1325
  var TYPESCRIPT_RUNNER = {
1303
- package: TypeScriptPackages.nativePreviewPinned,
1326
+ package: TypeScriptPackages.nativePreviewBeta,
1304
1327
  bin: "tsgo"
1305
1328
  };
1306
1329
  // ../constants/src/overworld.ts
@@ -1330,7 +1353,8 @@ var BADGES = {
1330
1353
  var TIMEBACK_ROUTES = {
1331
1354
  END_ACTIVITY: "/integrations/timeback/end-activity",
1332
1355
  GET_XP: "/integrations/timeback/xp",
1333
- HEARTBEAT: "/integrations/timeback/heartbeat"
1356
+ HEARTBEAT: "/integrations/timeback/heartbeat",
1357
+ ADVANCE_COURSE: "/integrations/timeback/advance-course"
1334
1358
  };
1335
1359
  // src/core/cache/singleton-cache.ts
1336
1360
  function createSingletonCache() {
@@ -1441,6 +1465,99 @@ function createRealtimeNamespace(client) {
1441
1465
  }
1442
1466
  };
1443
1467
  }
1468
+ // src/core/guards.ts
1469
+ var VALID_GRADES = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
1470
+ var VALID_SUBJECTS = [
1471
+ "Reading",
1472
+ "Language",
1473
+ "Vocabulary",
1474
+ "Social Studies",
1475
+ "Writing",
1476
+ "Science",
1477
+ "FastMath",
1478
+ "Math",
1479
+ "None"
1480
+ ];
1481
+ function isValidGrade(value) {
1482
+ return typeof value === "number" && Number.isInteger(value) && VALID_GRADES.includes(value);
1483
+ }
1484
+ function isValidSubject(value) {
1485
+ return typeof value === "string" && VALID_SUBJECTS.includes(value);
1486
+ }
1487
+
1488
+ // src/core/cache/ttl-cache.ts
1489
+ function createTTLCache(options) {
1490
+ const cache = new Map;
1491
+ const { ttl: defaultTTL, keyPrefix = "", onClear } = options;
1492
+ async function get(key, loader, config) {
1493
+ const fullKey = keyPrefix ? `${keyPrefix}:${key}` : key;
1494
+ const now = Date.now();
1495
+ const effectiveTTL = config?.ttl !== undefined ? config.ttl : defaultTTL;
1496
+ const force = config?.force || false;
1497
+ const skipCache = config?.skipCache || false;
1498
+ if (effectiveTTL === 0 || skipCache) {
1499
+ return loader();
1500
+ }
1501
+ if (!force) {
1502
+ const cached = cache.get(fullKey);
1503
+ if (cached && cached.expiresAt > now) {
1504
+ return cached.value;
1505
+ }
1506
+ }
1507
+ const promise = loader().catch((error) => {
1508
+ cache.delete(fullKey);
1509
+ throw error;
1510
+ });
1511
+ cache.set(fullKey, {
1512
+ value: promise,
1513
+ expiresAt: now + effectiveTTL
1514
+ });
1515
+ return promise;
1516
+ }
1517
+ function clear(key) {
1518
+ if (key === undefined) {
1519
+ cache.clear();
1520
+ onClear?.();
1521
+ } else {
1522
+ const fullKey = keyPrefix ? `${keyPrefix}:${key}` : key;
1523
+ cache.delete(fullKey);
1524
+ }
1525
+ }
1526
+ function size() {
1527
+ return cache.size;
1528
+ }
1529
+ function prune() {
1530
+ const now = Date.now();
1531
+ for (const [key, entry] of cache.entries()) {
1532
+ if (entry.expiresAt <= now) {
1533
+ cache.delete(key);
1534
+ }
1535
+ }
1536
+ }
1537
+ function getKeys() {
1538
+ const keys = [];
1539
+ const prefixLen = keyPrefix ? keyPrefix.length + 1 : 0;
1540
+ for (const fullKey of cache.keys()) {
1541
+ keys.push(fullKey.substring(prefixLen));
1542
+ }
1543
+ return keys;
1544
+ }
1545
+ function has(key) {
1546
+ const fullKey = keyPrefix ? `${keyPrefix}:${key}` : key;
1547
+ const cached = cache.get(fullKey);
1548
+ if (!cached) {
1549
+ return false;
1550
+ }
1551
+ const now = Date.now();
1552
+ if (cached.expiresAt <= now) {
1553
+ cache.delete(fullKey);
1554
+ return false;
1555
+ }
1556
+ return true;
1557
+ }
1558
+ return { get, clear, size, prune, getKeys, has };
1559
+ }
1560
+
1444
1561
  // ../utils/src/uuid.ts
1445
1562
  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
1563
  function isValidUUID(value) {
@@ -1450,7 +1567,7 @@ function isValidUUID(value) {
1450
1567
  return UUID_REGEX.test(value);
1451
1568
  }
1452
1569
 
1453
- // src/core/activity-tracker.ts
1570
+ // src/core/timeback/activity-tracker.ts
1454
1571
  var DEFAULT_PAUSED_HEARTBEAT_TIMEOUT_MS = 10 * 60 * 1000;
1455
1572
  var DEFAULT_HEARTBEAT_INTERVAL_MS = 15000;
1456
1573
  var DEFAULT_INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
@@ -1923,102 +2040,29 @@ function createTimebackActivityTracker(client) {
1923
2040
  };
1924
2041
  }
1925
2042
 
1926
- // src/core/cache/ttl-cache.ts
1927
- function createTTLCache(options) {
1928
- const cache = new Map;
1929
- const { ttl: defaultTTL, keyPrefix = "", onClear } = options;
1930
- async function get(key, loader, config) {
1931
- const fullKey = keyPrefix ? `${keyPrefix}:${key}` : key;
1932
- const now = Date.now();
1933
- const effectiveTTL = config?.ttl !== undefined ? config.ttl : defaultTTL;
1934
- const force = config?.force || false;
1935
- const skipCache = config?.skipCache || false;
1936
- if (effectiveTTL === 0 || skipCache) {
1937
- return loader();
1938
- }
1939
- if (!force) {
1940
- const cached = cache.get(fullKey);
1941
- if (cached && cached.expiresAt > now) {
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;
2043
+ // src/core/timeback/user.ts
2044
+ function createTimebackUserStore(client) {
2045
+ let override;
2046
+ return {
2047
+ snapshot() {
2048
+ return override ?? client["initPayload"]?.timeback;
2049
+ },
2050
+ async refresh() {
2051
+ const response = await client["request"]("/timeback/user", "GET");
2052
+ override = response;
2053
+ return {
2054
+ id: response.id,
2055
+ role: response.role,
2056
+ enrollments: response.enrollments,
2057
+ organizations: response.organizations
2058
+ };
1993
2059
  }
1994
- return true;
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);
2060
+ };
2017
2061
  }
2018
2062
 
2019
- // src/namespaces/game/timeback.ts
2020
- function createTimebackNamespace(client) {
2021
- const activityTracker = createTimebackActivityTracker(client);
2063
+ // src/core/timeback/engine.ts
2064
+ function createTimebackEngine(client) {
2065
+ const userStore = createTimebackUserStore(client);
2022
2066
  const userCache = createTTLCache({
2023
2067
  ttl: 5 * 60 * 1000,
2024
2068
  keyPrefix: "game.timeback.user"
@@ -2027,42 +2071,84 @@ function createTimebackNamespace(client) {
2027
2071
  ttl: 5000,
2028
2072
  keyPrefix: "game.timeback.xp"
2029
2073
  });
2030
- function getTimeback() {
2031
- return client["initPayload"]?.timeback;
2074
+ async function applyPromotion(promotion) {
2075
+ if (promotion.status !== "promoted" && promotion.status !== "already-promoted") {
2076
+ return;
2077
+ }
2078
+ userCache.clear("current");
2079
+ try {
2080
+ await userStore.refresh();
2081
+ } catch {}
2032
2082
  }
2083
+ const activityTracker = createTimebackActivityTracker(client);
2084
+ return {
2085
+ user: {
2086
+ snapshot: () => userStore.snapshot(),
2087
+ fetch: (options) => userCache.get("current", () => userStore.refresh(), options),
2088
+ xp: {
2089
+ fetch: (options) => {
2090
+ const cacheKey = [
2091
+ options.grade ?? "",
2092
+ options.subject ?? "",
2093
+ options.include?.toSorted().join(",") ?? ""
2094
+ ].join(":");
2095
+ return xpCache.get(cacheKey, async () => {
2096
+ const params = new URLSearchParams;
2097
+ if (options.grade !== undefined) {
2098
+ params.set("grade", String(options.grade));
2099
+ }
2100
+ if (options.subject !== undefined) {
2101
+ params.set("subject", options.subject);
2102
+ }
2103
+ if (options.include?.length) {
2104
+ params.set("include", options.include.join(","));
2105
+ }
2106
+ const queryString = params.toString();
2107
+ const endpoint = `${TIMEBACK_ROUTES.GET_XP}${queryString ? `?${queryString}` : ""}`;
2108
+ return client["requestGameBackend"](endpoint, "GET");
2109
+ }, { force: options.force });
2110
+ }
2111
+ }
2112
+ },
2113
+ activity: {
2114
+ start: activityTracker.startActivity,
2115
+ pause: activityTracker.pauseActivity,
2116
+ resume: activityTracker.resumeActivity,
2117
+ end: activityTracker.endActivity
2118
+ },
2119
+ async advanceCourse(params) {
2120
+ const response = await client["requestGameBackend"](TIMEBACK_ROUTES.ADVANCE_COURSE, "POST", params?.subject !== undefined ? { subject: params.subject } : {});
2121
+ await applyPromotion(response.promotion);
2122
+ return response;
2123
+ }
2124
+ };
2125
+ }
2126
+
2127
+ // src/namespaces/game/timeback.ts
2128
+ var VALID_INCLUDE_OPTIONS = ["perCourse", "today"];
2129
+ function createTimebackNamespace(client) {
2130
+ const engine = createTimebackEngine(client);
2033
2131
  return {
2034
2132
  get user() {
2035
2133
  assertPlatformMode(client, "timeback.user");
2036
2134
  return {
2037
2135
  get id() {
2038
- return getTimeback()?.id;
2136
+ return engine.user.snapshot()?.id;
2039
2137
  },
2040
2138
  get role() {
2041
- return getTimeback()?.role;
2139
+ return engine.user.snapshot()?.role;
2042
2140
  },
2043
2141
  get enrollments() {
2044
- return getTimeback()?.enrollments ?? [];
2142
+ return engine.user.snapshot()?.enrollments ?? [];
2045
2143
  },
2046
2144
  get organizations() {
2047
- return getTimeback()?.organizations ?? [];
2145
+ return engine.user.snapshot()?.organizations ?? [];
2048
2146
  },
2049
- fetch: async (options) => userCache.get("current", async () => {
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),
2147
+ fetch: (options) => engine.user.fetch(options),
2062
2148
  xp: {
2063
- fetch: async (options) => {
2064
- const hasGrade = options?.grade !== undefined;
2065
- const hasSubject = options?.subject !== undefined;
2149
+ fetch: async (options = {}) => {
2150
+ const hasGrade = options.grade !== undefined;
2151
+ const hasSubject = options.subject !== undefined;
2066
2152
  if (hasGrade !== hasSubject) {
2067
2153
  throw new Error("Both grade and subject must be provided together");
2068
2154
  }
@@ -2072,53 +2158,40 @@ function createTimebackNamespace(client) {
2072
2158
  if (hasSubject && !isValidSubject(options.subject)) {
2073
2159
  throw new Error(`Invalid subject: ${options.subject}. Valid subjects: ${VALID_SUBJECTS.join(", ")}`);
2074
2160
  }
2075
- const validIncludeOptions = ["perCourse", "today"];
2076
- if (options?.include?.length) {
2161
+ if (options.include?.length) {
2077
2162
  for (const opt of options.include) {
2078
- if (!validIncludeOptions.includes(opt)) {
2079
- throw new Error(`Invalid include option: ${opt}. Valid options: ${validIncludeOptions.join(", ")}`);
2163
+ if (!VALID_INCLUDE_OPTIONS.includes(opt)) {
2164
+ throw new Error(`Invalid include option: ${opt}. Valid options: ${VALID_INCLUDE_OPTIONS.join(", ")}`);
2080
2165
  }
2081
2166
  }
2082
2167
  }
2083
- const cacheKey = [
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 });
2168
+ return engine.user.xp.fetch(options);
2103
2169
  }
2104
2170
  }
2105
2171
  };
2106
2172
  },
2107
2173
  startActivity: (metadata, options) => {
2108
2174
  assertPlatformMode(client, "timeback.startActivity()");
2109
- activityTracker.startActivity(metadata, options);
2175
+ engine.activity.start(metadata, options);
2110
2176
  },
2111
2177
  pauseActivity: () => {
2112
2178
  assertPlatformMode(client, "timeback.pauseActivity()");
2113
- activityTracker.pauseActivity();
2179
+ engine.activity.pause();
2114
2180
  },
2115
2181
  resumeActivity: () => {
2116
2182
  assertPlatformMode(client, "timeback.resumeActivity()");
2117
- activityTracker.resumeActivity();
2183
+ engine.activity.resume();
2118
2184
  },
2119
2185
  endActivity: async (data) => {
2120
2186
  assertPlatformMode(client, "timeback.endActivity()");
2121
- return activityTracker.endActivity(data);
2187
+ return engine.activity.end(data);
2188
+ },
2189
+ advanceCourse: async (options) => {
2190
+ assertPlatformMode(client, "timeback.advanceCourse()");
2191
+ if (options?.subject !== undefined && !isValidSubject(options.subject)) {
2192
+ throw new Error(`Invalid subject: ${options.subject}. Valid subjects: ${VALID_SUBJECTS.join(", ")}`);
2193
+ }
2194
+ return engine.advanceCourse(options);
2122
2195
  }
2123
2196
  };
2124
2197
  }